--- id: flipper-plugin title: Desktop Plugin API --- ## PluginClient `PluginClient` is the type of the `client` passed into a standard Sandy plugin. It takes two generic arguments `Event` and `Methods`. * The `Event` generic is a mapping of an event name to the data structure of the payload, as explained [here](../tutorial/js-custom.mdx#the-plugin-declaration). * The `Methods` generic is used to describe the methods that are offered by the plugin implementation on the device. `Methods` is a mapping of a method name to a function that describes the signature of a method. The first argument of that function describes the parameters that can be passed to the client. The return type of the function should describe what is returned from the client. Wrapped with a `Promise`. Quick example on how those generics should be used: ```typescript type LogEntry = { message: string } // Events that can be send by the client implementation: type Events = { addLogEntry: LogEntry, flushLogs: {}, } // Methods we can invoken on the client: type Methods = { retrieveLogsSince(params: { since: number }): Promise<{ message: string }>, } export function plugin(client: PluginClient) { // etc } ``` The `PluginClient` received by the `plugin` exposes the following members: ### Properties #### `device` Returns the [`Device`](#device) this plugin is connected to. #### `appName` The name of the application, for example 'Facebook', 'Instagram' or 'Slack'. #### `appId` A string that uniquely identifies the current application, is based on a combination of the application name and device serial on which the application is running. #### `pluginKey` A key that uniquely identifies this plugin instance, captures the current device/client/plugin combination. #### `connected` #### `isConnected` Returns whether there is currently an active connection. This is true if: 1. The device is still connected 2. The client is still connected 3. The plugin is currently selected by the user _or_ the plugin is running in the background. The `connected` field provides the atom, that can be used in combination with `useValue` to subscribe to future updates in a component. In contrast, `isConnected` returns a boolean that merely captures the current state. ### Events listeners #### `onMessage` Usage: `client.onMessage(event: string, callback: (params) => void)` This subscribes the plugin to a specific event that is fired from the client plugin (using [`connection.send`](../extending/create-plugin#push-data-to-the-desktop)). Typically used to update some of the [state](#createstate). For background plugins that are currently not active in the UI, messages won't arrive immediately, but are queued until the user opens the plugin. Example: ```typescript type Events = { newRow: { id: number; title: string; url: string; }; }; export function plugin(client: PluginClient) { const rows = createState>({}, {persist: 'rows'}); client.onMessage('newRow', (row /* type will be inferred correctly */) => { rows.update((draft) => { draft[row.id] = row; }); }); // etc } ``` #### `onUnhandledMessage` Usage: `client.onUnhandledMessage(callback: (event: string, params) => void)` This method subscribe to all messages arriving from the devices which is not handled by an `onMessage` handler. This handler is untyped, and onMessage should be favored over using onUnhandledMessage if the event name is known upfront. #### `onActivate` Usage: `client.onActivate(callback: () => void)` Called when the plugin is selected by the user and mounted into the Flipper Desktop UI. See also the closely related `onConnect` event. #### `onDeactivate` Usage: `client.onDeactivate(callback: () => void)` Triggered when the plugin is unmounted from the Flipper Desktop UI, because the user navigates to some other plugin. In the case the plugin is destroyed while being active, onDeactivate will still be called. #### `onConnect` Usage: `client.onConnect(callback: () => void)` Triggered once the connection with the plugin on the client is established, and for example [`send`](#send) can be called safely. Typically, this happens when the plugin is activated (opened) in the Flipper Desktop. However, for [background plugins](create-plugin#background-plugins), this happens immediately after the plugin has been instantiated. #### `onDisconnect` Usage: `client.onDisconnect(callback: () => void)` Triggered once the connection with the plugin on the client has been lost. Typically, this happens when the user leaves the plugin in the Flipper Desktop, when the plugin is disabled, or when the app or device has disconnected. However, for [background plugins](create-plugin#background-plugins), this event won't fire when the user merely navigates somewhere else. In that case, [`onDeactivate`](#ondeactivate) can be used instead. #### `onDestroy` Usage: `client.onDestroy(callback: () => void)` Called when the plugin is unloaded. This happens if the device or client has been disconnected, or when the user disables the plugin. Note that there is no corresponding `onCreate` event, since the function body of the plugin definition acts already as 'what needs to be done when the plugin is loaded/enabled'. #### `onDeepLink` Usage: `client.onDeepLink(callback: (payload: unknown) => void)` Trigger when the users navigates to this plugin using a deeplink, either from an external `flipper://` plugin URL, or because the user was linked here from another plugin. #### `onExport` Usage: `client.onExport(callback: (idler, onStatusMessage) => Promise)` Overrides the default serialization behavior of this plugin. Should return a promise with persistable state that is to be stored, or nothing at all. This process is async, so it is possible to first fetch some additional state from the device. Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects, arrays or Date, Set or Map objects. If nothing is returned, the handler will be run, and after the handler has finished the `persist` keys of the different states will be used as export basis. #### `onImport` Usage: `client.onImport(callback: (snapshot) => void)` Overrides the default de-serialization behavior of this plugin. Use it to update the state based on the snapshot data. This hook will be called immediately after constructing the plugin instance. To synchonize the types of the data between `onImport` and `onExport`, it is possible to provide a type as generic to both hooks. The next example stores `counter` under the `count` field, and stores it as string rather than as number. ```typescript type SerializedState = { count: string; } export function plugin(client: PluginClient) { const counter = createState(0); client.onExport(() => { return { count: "" + counter.get() } }) client.onImport((data) => { counter.set(parseInt(data.count, 10)); }); } ``` ### Methods #### `send` Usage: `client.send(method: string, params: object): Promise` If the plugin is connected, `send` can be used to invoke a [method](create-plugin#[background-plugins#using-flipperconnection) on the client implementation of the plugin. Note that if `client.isConnected` returns `false`, calling `client.send` will throw an exception. This is the case if for example the connection with the device or application was lost. Generally one should guard `client.send` calls with a check to `client.isConnected`. Example: ```typescript type Methods = { currentLogs(params: {since: number}): Promise; }; export function plugin(client: PluginClient<{}, Methods>) { const logs = createState([]) client.onConnect(async () => { try { const currentLogs = await client.send('currentLogs', { since: Date.now() }) logs.set(currentLogs) } catch (e) { console.error("Failed to retrieve current logs: ", e) } }) //etc } ``` #### `addMenuEntry` Usage: `client.addMenuEntry(...entry: MenuEntry[])` This method can be used to add menu entries to the Flipper main menu while this plugin is active. It supports registering global keyboard shortcuts as well. Example: ```typescript client.addMenuEntry({ label: 'Reset Selection', topLevelMenu: 'Edit', accelerator: 'CmdOrCtrl+R' handler: () => { // Event handling } } ``` The `accelerator` argument is optional, but describes the keyboard shortcut. See the [Electron docs](https://www.electronjs.org/docs/api/accelerator) for their format. The `topLevelMenu` must be one of `"Edit"`, `"View"`, `"Window"` or `"Help"`. It is possible to leave out the `label`, `topLevelMenu` and `accelerator` fields if a pre-defined `action` is set, which configures all three of them. The currently pre-defined actions are `"Clear"`, `"Go To Bottom"` and `"Create Paste"`. Example of using a pre-defined action: ```typescript client.addMenuEntry({ action: 'createPaste', handler: async () => { // Event handling } }) ``` #### `isPluginAvailable` Usage: `isPluginAvailable(pluginId: string): boolean` Returns `true` if a plugin with the given id is available by for consumption, that is: supported by the current application / device, and enabled by the user. #### `selectPlugin` Usage: `selectPlugin(pluginId: string, deeplinkPayload?: unknown): void` Opens a different plugin by id, optionally providing a deeplink to bring the target plugin to a certain state. #### `supportsMethod` Usage: `client.supportsMethod(method: string): Promise` Resolves to true if the client supports the specified method. Useful when adding functionality to existing plugins, when connectivity to older clients is still required. Also useful when client plugins are implemented on multitple platforms and don't all have feature parity. #### showNotification Usage: `client.showNotification(notification)` Shows an urgent, system wide notification, that will also be registered in Flipper's notification pane. For on-screen notifications, we recommend to use either the `message` or `notification` API from `antd` directly. Clicking the notification will open the sending plugin. If the `action` id is set, it will be used as deeplink. The notification interface is defined as: ```typescript interface Notification { id: string; title: string; message: string | React.ReactNode; severity: 'warning' | 'error'; timestamp?: number; category?: string; action?: string; }; ``` #### `writeTextToClipboard` Usage: `writeTextToClipboard(text: string)` Writes text to the OS-level clipboard. #### `createPaste` Facebook only API. Usage: `client.createPaste(value: string): Promise` Creates a Facebook Paste (similar to a GitHub Gist) for the given `value`. The returned promise either contains a string with the URL of the paste, or `undefined` if the process failed. Details of the failure will be communicated back directly to the user through Flipper notifications. For example if the user is currently not signed in. #### `GK` Facebook only API. Usage: `client.GK(gatekeeper: string): boolean` Returns `true` if the current user is part of the given GK. `false` in all other cases. To use a gatekeeper in the Facebook build of Flipper: 1. [Create the gatekeeper](https://www.internalfb.com/intern/gatekeeper/) 2. Add the Gatekeepers name to the `subscribedGatekeepers` array in `fbsource/xplat/sonar/desktop/src/fb/GK.tsx` 3. Use `client.GK('name_of_gk')` to read the gatekeeper, or alternatively: `import {GK} from 'flipper'; GK.get('name_of_gk')` The gatekeepers are cached in `localStorage`. Therefore, you might get the cached result of the gatekeeper on the first access (which defaults to `false`). On the next start of the app you will get the new result. #### `logger` Logger instance that logs information to the console, but also to the internal logging (in FB only builds) and which can be used to track performance. See also [`useLogger`](#uselogger). ## DevicePluginClient ### Properties #### `device` Returns the [`Device`](#device) this plugin is connected to. ### Events #### `onDestroy` See the similarly named event under [`PluginClient`](#pluginclient). #### `onActivate` See the similarly named event under [`PluginClient`](#pluginclient). #### `onDeactivate` See the similarly named event under [`PluginClient`](#pluginclient). #### `onDeepLink` See the similarly named event under [`PluginClient`](#pluginclient). #### `onExport` See the similarly named event under [`PluginClient`](#pluginclient). #### `onImport` See the similarly named event under [`PluginClient`](#pluginclient). ### Methods #### `addMenuEntry` See the similarly named method under [`PluginClient`](#pluginclient). #### `createPaste` See the similarly named method under [`PluginClient`](#pluginclient). #### `showNotification` See the similarly named method under [`PluginClient`](#pluginclient). ### `isPluginAvailable` See the similarly named method under [`PluginClient`](#pluginclient). ### `selectPlugin` See the similarly named method under [`PluginClient`](#pluginclient). ## Device `Device` captures the metadata of the device the plugin is currently connected to. Device objects are passed into the [`supportsDevice` method](../tutorial/js-custom#creating-a-device-plugin) of a device plugin, and available as `device` field on a [`DevicePluginClient`](#devicepluginclient). ### Properties #### os A `string` that describes the Operating System of the device. Typical values: `'iOS'` | `'Android'` | `'Windows'` | `'MacOS'` | `'Metro'` #### deviceType A `string` that describes whether the device is a physical device or an emulator. Possible values: `'emulator'` and `'physical'`. #### isArchived This `boolean` flag is `true` if the current device is coming from an import Flipper snapshot, and not an actually connected device. #### isConnected This `boolean` flag is `true` if the connection to the device is still alive. ### Events #### `onLogEntry` Usage: `device.onLogEntry(callback: (logEntry: DeviceLogEntry) => void)` Use this event to subscribe to the log stream that is emitted by the device. For Android this is using `adb` behind the scenes, for iOS `idb`, for Metro it connects to the webserver for the Metro log output, etc. The `DeviceLogEntry` exposes the following fields: * `date: Date` * `type: string` * `message: string` * `pid: number` * `tid: number` * `app?: string` * `tag: string` For `type`, the possible values are `'unknown'`, `'verbose'`, `'debug'`, `'info'`, `'warn'`, `'error'` and `'fatal'`. ## State Management State in Sandy plugins is stored in small containers that hold immutable values, and can be consumed in React components using the [`useValue`](#usevalue) hook. ### createState Usage: `createState(initialValue: T, options?): StateAtom` The `createState` method can be used to create a small state container that lives inside a Sandy plugin. Its value should be treated as immutable and is initialized by default using the `initialValue` parameter. #### Options Optionally, `options` can be provided when creating state. Supported options: * `persist: string`. If the `persist` value is set, this state container will be serialized when n Flipper snapshot export is being made. When a snapshot is imported into Flipper, and plugins are initialized, this state container will load its initial value from the snapshot, rather than using the `initialValue` parameter. The `persist` key should be unique within the plugin and only be set if the state stored in this container is serializable and won't become unreasonably large. See also `exportState` and `initialState` in the [`TestUtils`](#testutils) section. Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects, arrays or Date, Set or Map objects. #### The state atom object A state atom object is returned by `createState`, exposing the following methods: * `get(): T`: Returns the current value stored. If you want to use the atom object in a React component, consider using the `useValue` hook instead, to make sure the component is notified about future updates of this atom. * `set(newValue: T)`: Stores a new value into the atom. If the new value is not reference-equal to the previous one, all observing components will be notified. * `update(updater: (draft: Draft) => void)`: Updates the current state using an [Immer](https://immerjs.github.io/immer/docs/introduction) recipe. In the `updater`, the `draft` object can be safely (deeply) mutated. Once the `updater` finishes, Immer will compute a new immutable object based on the changes, and store that. This is often simpler than using a combination of `get` and `set` if deep updates need to be made to the stored object. * `subscribe(listener: (value: T, prevValue: T) => void): () => void`: Subscribes a listener function to the state updates. Listener function will receive the next and previous value on each update. The method also returns function which can be called to unsubscribe the listener from further updates. * `unsubscribe(listener: (value: T, prevValue: T) => void): void`: Unsubscribes a listener function from the state updates if it was subscribed before. #### Example ```typescript import {createState} from 'flipper-plugin' const rows = createState([], {persist: 'rows'}); const selectedID = createState(null, {persist: 'selection'}); // Listener will be called on each rows.set() and rows.update() call until unsubscribed. const unsubscribe = rows.subscribe((value, prevValue) => { console.log(`Rows state updated. New length: ${value.length}. Prev length: ${prevValue.length}.`); }); rows.set(["hello"]) // Listener will be notified about the change console.log(rows.get().length) // 1 unsubscribe(); // Do not notify listener anymore rows.update(draft => { // Listener won't be notified about the change draft.push("world") }) console.log(rows.get().length) // 2 ``` ### createDataSource Usage: `createDataSource(initialSet?: T[], options?): DataSource` Most Flipper plugins follow the basic concept of receiving events from the device, store them, and being able to tail, filter and search them. To optimise for this situation, there is a dedicated `createDataSource` abstraction which creates a `DataSource`. `DataSource` is a data collection that is heavily optimized for `append` and `update`, which stores items based on insertion order, but also allows for efficient by-id lookups. Each `DataSource` exposes a `view` property, which contains a `DataSourceView`. A `DataSourceView` is a materialized view which can be sorted, filtered and windowed, and will be kept incrementally up to date with the underlying `DataSource`. When using the `DataTable` component, this `view` will be managed by the table automatically, giving plugin users the capability to freely sort, filter, search and tail your datasource. Valid `options` are: * `key`: If a key is set, the given field of the records is assumed to be unique, and it's value can be used to perform lookups and upserts. * `limit`: The maximum amount of records that this DataSource will store. If the limit is exceeded, the oldest records will automatically be dropped to make place for the new ones. Defaults to 100.000 records. * `persist`: See the `createState` `persist` option: If set, this data source will automatically be part of Flipper imports / exports. It is recommend to set this option. All records stored in a data source should be treated as being immutable. To update a record, replace it with a new value using the `update` or `upsert` operations. #### Example ```typescript export function devicePlugin(client: DevicePluginClient) { const rows = createDataSource([], { limit: 200000, persist: 'logs', }); client.device.onLogEntry(entry => { rows.append(entry); }); return { rows, } } ``` ### DataSource Stores large amounts of records efficiently. See [`createDataSource`](#createdatasource) for an introduction. #### limit The maximum amount of records that can be stored in this DataSource to constrain memory usages. Defaults to 100.000 rows. If the limit is exceeded, the oldest 10% of records is dropped. This field is writable but does not immediately truncate if changed. #### view Returns the currently active view on the data source. Note that be default it is windowed on the impractical `[0, 0)` range. See [`DataSourceView`](#datasourceview) for more details. #### size The total amount of records stored in this data source. #### records Usage: `records(): T[]`. Returns all values stored in this data source in a defensive copy. Note that this operation performs `O(n)`, so typically one should operate on a subset of the records using `size` and `get`. #### get Usage: `get(index: number): T`. Returns the record at the given index, which is insertion order based. This operation does not take into consideration the current view. See also `view.get` to get a record based on _visible_ position. To look items up based on their id, use `getById`. #### getById Usage: `getById(key: string): T | undefined`. For example `users.getById("jane")`. Returns the record associated with the given key, or `undefined`. This method can only be used if the `key` option was passed to `createDataSource`. #### keys Usage: `keys()`, returns an iterator that will iterate all keys in the data source. For example to create an array of all keys: `const usernames = Array.from(users.keys())`. This method can only be used if the `key` option was passed to `createDataSource`. #### entries Usage: `entries()`. Similar to `keys()`, but will return an iterator that generate entry tuples, in the shape of `[key, value]`. #### [Symbol.iterator] `DataSource` supports the iterator protocol, so to visit all stored records one can use `for (const user of users) {....}`. #### getIndexOfKey Usage: `getById(key: string): number`. Returns the insertion index of the record associated with the given key, or `-1`. This method can only be used if the `key` option was passed to `createDataSource`. #### append Usage: `append(record: T)`. Appends a new record to the data collection. This method will throw if a duplicate key is inserted. Use `upsert` to automatically append *or* update. Mutations like `append` will be reflected in the `view` automatically. #### update Usage: `update(index: number, record: T)`. Replaces the given record in the data sources. #### delete Usage: `delete(index: number)`. Remove the record at the given index from the datasource. Note that if a the `key` option of the datasource is set, this operation degrades to `O(n)` performance and should typically be avoided. #### deleteById Usage: `delete(key: string): boolean`. Removes the record with the given key. Returns `true` if the record existed and has been removed. This operation is `O(n)` expensive and should generally be avoided. #### shift Usage: `shift(amount: number)`. Removes the first `amount` records from the datasource. This is generally a performant operation. #### clear Usage: `clear()`. Removes all records from this data source. #### fork Usage: `fork(): DataSourceView`. Creates an additional materialized view on this data source with it's own sort / filter settings. This feature is not implemented yet so contact Flipper oncall if needed. ### DataSourceView A materialized view on a DataSource, which can apply windowing, sorting and filtering and will be kept incrementally up to date with the underlying datasource. Note that the default window is empty, so after obtaining a `DataSourceView` one should typically call `setWindow`. See [`createDataSource`](#createdatasource) for an introduction. The DataSourceView API is important if are creating your own visualization of a `DataSource`. However, if a `DataSource` is visualized using a `DataTable`, there is typically no need to directly interact with this API. #### datasource A reference to the underlying [`DataSource`](#datasource). #### windowStart See `setWindow` #### windowEnd See `setWindow` #### size The total size of the current view after applying filtering. Note that `size` does _not_ reflect windowing. To get the window size use: `windowEnd - windowStart`. To get the total amount of records, without respecting the current filter, use `datasource.size`. #### isSorted Returns `true` if a sort criterium is set. #### isFiltered Returns `true` if a filter criterium is set. #### isRevered Return `true` if the current view will be shown in reverse order. #### output Usage: `output(): T[]` or `output(start, end): T[]`. Returns a defensive copy of all items visible in the provided range window. If `start` and `end` are omitted, the current window will be used. To get all items visible in the current view, ignoring the window, use `view.output(0, view.size)`. #### [Symbol.iterator] `DataSourceView` supports the iterator protocol, so the currently visible output can be iterated using for example `for (const user in users.view) { ... }`. The iterator will always apply the current window. #### setWindow Usage: `setWindow(start, end)`. This method sets the current visible window to the specified range (which will include `start`, but not `end`, so `[start, end)`). Setting a window impacts the default behavior of `output` and `iterator` and, more importantly, the behavior of any listener: `update` events that happen outside the window will not be propagated to any listeners, and `shift` events will describe whether the happened `in`, `before`, or `after` the current window. Windowing will always be applied only after applying any filters, sorting and reversing. #### setFilter Usage: `setFilter(filter: (record: T) => boolean)`. Applies a filter to the current records. This will typically reduce `size` of this view. Example: `users.view.setFilter(user => user.age >= 18)`. #### setSortBy Usage: `setSortBy(field: string)` or `setSortBy(sortBy: (irecord: T) => primitive)`. For example: `users.view.setSortBy("age")` or `users.viewSetSortBy(user => `${user.lastName} ${user.firstName}`)`. `setSortBy` will cause the data source to be sorted by the given field or criterium function. Sort is implemented efficiently by using a binary search to insert / remove newly arriving records, rather than performing a full sort. But this means that the sort function should be stable and pure. Sorting will always happen in ascending order, and if duplicate sort values appear, the insertion order will take precedence. To sort in descending order, use `setReversed`. If a view doesn't have sorting specified, it will always show records in insertion order. #### toggleRevered Usage: `toggleReversed()`. Toggles the output order between ascending and descending. #### setReversed Usage: `setReversed(ascending: boolean)`. Defines whether the output colletion is shown normal (ascending) or reverse (descending) order. #### reset Usage: `reset()`. Resets the window, filtering, sorting and reverse to their defaults. Note that this puts the window back to `[0, 0)` as well, meaning now recordswill be part of the output. #### get Usage: `get(index: number)`. Returns the record at the given position in the output. The `index` parameter respects sorting, filtering and reversing, but does _not_ respect any window offset. So `get(0)` will return the first record in the datasource according the given filtering, sorting and reversing, while `get(windowStart)` will return the first of the records visible in the current window. #### setListener Usage: `setListener(callback: undefined | (event: OutputChange) => void)`. Sets up a listener that will get notified whenever the `output` or `size` of this view changes. This can be used to, for example, update the UI and is used by `DataTable` under the hood. The following events can be emitted. These events respect the current sorting, filtering and reversing. The shift `location` is expressed relatively to the current window. Now `update` events that are outside the current window will be emitted. `reset` events are typically emitted if a change happened that cannot be expressed in a limited amount of shifts / updates. Such as changing sorting or filtering, calling `clear()` or `reset()`, or doing a large `shift`. Currently only one listener is allowed at a time. Please contact the Flipper oncall if that doesn't suffice. ```typescript type OutputChange = | { type: 'shift'; index: number; // the position at which records were inserted or removed location: 'before' | 'in' | 'after'; // relative to current window delta: number; // how many records were inserted (postive number) or removed (negative number) newCount: number; // the new .size of the DataSourceView } | { // an item, inside the current window, was changed type: 'update'; index: number; } | { // something big and awesome happened. Drop earlier updates to the floor and start again // like: clear, filter or sorting change, etc type: 'reset'; newCount: number; }; ``` ## React Hooks ### usePlugin Usage: `const instance = usePlugin(plugin)` Can be used by any component in the plugin, and gives the current `instance` that corresponds with the currently loaded plugin. The `plugin` parameter isn't actually used, but used to verify that a component is used correctly inside a mounted component, and helps with type inference. The returned `instance` method corresponds to the object that is returned from the `plugin` / `devicePlugin` definition. See the [tutorial](../tutorial/js-custom#building-an-user-interface-for-the-plugin) for how this hook is used in practice. ### useValue Usage: `const currentValue = useValue(stateAtom)` Returns the current value of a state atom, and also subscribes the current component to future changes of the atom (in contrast to using `stateAtom.get()` directly). See the [tutorial](../tutorial/js-custom#building-an-user-interface-for-the-plugin) for how this hook is used in practice. ### useLogger Usage: `const logger = useLogger()` Provides the default logger that can be used for console logging, error reporting and performance measurements. In internal Facebook builds this is wired up to the internal statistic reporting. Prefer using `logger` over using `console` directly. The logger API is defined as: ```typescript interface Logger { track(type: TrackType, event: string, data?: any, plugin?: string): void; trackTimeSince( mark: string, eventName?: string | null | undefined, data?: any, ): void; info(data: any, category: string): void; warn(data: any, category: string): void; error(data: any, category: string): void; debug(data: any, category: string): void; } ``` ### useTrackedCallback Usage: `const eventHandler = useTrackedCallback("Interaction description", handlerFunction, deps)` Utility that wraps React's `useCallback` with tracking capabilities. The API is similar, except that the first argument describes the interaction handled by the given event handler. See [Tracked](#tracked) for more info. ### useMemoize Slight variation on useMemo that encourages to create hoistable memoization functions, which encourages reuse and testability by no longer closing over variables that are used by the memoized function, but rather receiving them as arguments so that these functions beome pure. ```javascript function MyComponent() { const {findMetroDevice} = props; const connections = useSomeHook(); const metroDevice = useMemoize( findMetroDevice, [connections.devices], ); // etc } export function findMetroDevice(findMetroDevice, deviceList) { return deviceList.find(findMetroDevice); } ``` ### useLocalStorageState Like `useState`, but the value will be stored in local storage under the given key, and read back upon initialization. The hook signature is similar to `useState`, except that the first argument is the storage key. The storage key will be scoped automatically to the current plugin and any additional tracking scopes. (See [`TrackingScope`](#trackingscope)). ```typescript const [showWhitespace, setShowWhitespace] = useLocalStorageState( `showWhitespace`, true ); ``` ## UI components ### Layout.* Layout elements can be used to organize the screen layout. See `View > Flipper Style Guide` inside the Flipper application for more details. ### DataTable ### DataFormatter ### DataInspector ### DataDescription ### MarkerTimeline ### ElementsInspector ### ElementAttribute ### ElementData ### ElementExtraInfo ### ElementID ### ElementSearchResultSet ### ElementsInspectorElement ### ElementsInspectorProps ### Panel A collapsible UI region. The collapsed state of the pane will automatically be persisted so that the collapsed state is restored the next time user visits the plugin again. Note that the children of a Panel should have some size, either a fixed or a natural size. Elements that grow to their parent size will become invisible. For demos and property documentation see the 'Flipper style guide'. ### Tabs ### Tab The `Tabs` and `Tab` component re-expose the TabControl as provided by Antd, and support the same properties. See [official docs](https://ant.design/components/tabs/). The following additional properties are supported: * `grow`. If set, the tab control will use all available vertical space. By default `false`. ### Toolbar A horizontal panel to organize buttons and alike. Basically a `Layout.Horizontal` with a border, padding, gap and wrapping enabled. ### DataList The DataList can be used to display a set of items efficiently, and where a single item can be selected. Properties: * `items`: Items to display. Per item at least a title and unique id should be provided. * `value`: The current selection * `onSelect` * `onRenderItem`: A custom render function. By default the component will render the `title` in bold and description (if any) below it. * `type`: `default` or `dropdown. Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop down * `scrollable`: By default the data list will take all available space and scroll if items aren't otherwise visible. By setting `scrollable={false}` the list will only take its natural size ### NUX An element that can be used to provide a New User eXperience: Hints that give a one time introduction to new features to the current user. See `View > Flipper Style Guide` inside the Flipper application for more details. ### DetailSidebar An element that can be passed children which will be shown in the right sidebar of Flipper. Horizontal scrolling will be enabled by default. To fine-tune the default dimensions use `width` and `minWidth`. It doesn't really matter where exactly this component is used in your layout, as the contents will be moved to the main Flipper chrome, rather than being rendered in place. ### Tracked An element that can be used to track user interactions. An example scuba query can be found [here](https://fburl.com/scuba/infinity_analytics_events/xryoq5j7). See `View > Flipper Style Guide` inside the Flipper application for more details. ### TrackingScope Defines the location of underlying Tracked elements more precisely. See `View > Flipper Style Guide` inside the Flipper application for more details. ### withTrackingScope Higher order component that wraps a component automatically in a [`TrackingScope`](#TrackingScope) using the component name as `scope`. ### theme object Provides a standard set of colors and spacings, used by the Flipper style guide. The colors exposed here support dark mode. See `View > Flipper Style Guide` inside the Flipper application for more details. ## Utilities ### createTablePlugin Utility to create a plugin that consists of a master table and details json view with minimal effort. See [../tutorial/js-table.mdx](Showing a table) for more details. ### batch Usage: `batch(() => { /* state updates */ })` Low-level utility to batch state updates to reduce the amount of potential re-renders by React. Wraps React's `unstable_batchedUpdates`. Event handlers provided by React or `flipper-plugin` already apply `batch` automatically, so using this utility is only recommended when updating plugin state in an asynchronous process. ### produce A convenience re-export of `produce` from [Immer](https://immerjs.github.io/immer/docs/produce). The `update` method of the state atoms returned by `createState` automatically applies `produce` to its updater function. ### renderReactRoot Usage: `renderReactRoot(handler: (unmount: () => void) => React.ReactElement)` Renders an element outside the current DOM tree. This is a low-level utility that can be used to render for example Modal dialogs. The provided `handler` function should return the root element to be rendered. Once the element can be removed from the DOM, the `unmount` callback should be called. Example: ```typescript renderReactRoot((unmount) => ( )); ``` ## sleep Usage: `await sleep(1000)` Creates a promise that automatically resolves after the specified amount of milliseconds. ## styled A convenience re-export of `styled` from [emotion](https://emotion.sh/docs/styled). ## TestUtils The object `TestUtils` as exposed from `flipper-plugin` exposes utilities to write unit tests for Sandy plugins. Different utilities are exposed depending on whether you want to test a client or device plugin, and whether or not the component should be rendered or only the logic itself is going to be tested. It is recommended to follow the [tutorial](../tutorial/js-custom) first, as it explains how unit tests should be setup. ### Starting a plugin Usage: - `const runner = TestUtils.startPlugin(pluginModule, options?)` - `const runner = TestUtils.renderPlugin(pluginModule, options?)` - `const runner = TestUtils.startDevicePlugin(devicePluginModule, options?)` - `const runner = TestUtils.renderDevicePlugin(devicePluginModule, options?)` Starts a client plugin in a fully mocked environment, but without rendering support. The pluginModule is an object that has a `plugin` (or `devicePlugin` and `supportsDevice`) and `Component` property. Typically, it is invoked with `startPlugin(PluginUnderTest)`, where `PluginUnderTest` is loaded like `import * as PluginUnderTest from "../index.tsx"` (the path to the actual definition). However, it doesn't have to be loaded with an entire module, and a local object with the same signature can be constructed as well. #### startPlugin options The `options` argument is optional, but can specify the following fields: * `initialState`: Can be used to start the plugin in a certain state, rather than in the default state. `initialState` should be an object that specifies for all the state atoms that have the `persist` option set, their initial value. For example: `{ initialState: { rows: ["hello", "world"]}}`, where `rows` matches the `persist` key of an atom. * `isArchived: boolean`: Setting this flag, will set the `isArchived` on the mocked device as well. Set it if you want to test the behavior of your plugin for imported devices (see also [`Device.isArchived`](#isarchived)). Defaults to `false`. * `isBackgroundPlugin`: This makes sure the test runner emits life-cycle events in a way that is typical for background plugins. Defaults to `false`. The notable difference in behavior is that calling `.active()` on the test runner won't trigger the `connect` event to be fired, nor the `.deactivate()` the `disconnect` event. * `startUnactivated`: This does not activate the plugin; `connect` needs to be explicitly called. This can be used in case setting mock implementation for `onSend` is required to make sure Client plugin works as expected. Defaults to `false`. * `GKs`: A string array of gatekeeper names for which `client.GK` will `true` inside the test. By default GKs are assumed to be disabled inside unit tests. #### The test runner object `startPlugin` returns an object that can be used to inspect and interact with your plugin instance. Again, see the tutorial how to interact with this object in general. The test runner is a bag full of utilities, but typically it is fine to just destructure the utilities relevant for the test. Exposed members: * `instance`: The object (public API) returned from your plugin definition. You will typically use this in most tests, either to trigger updates or to inspect the current state of the plugin. * `exportState()`: Grabs the current state of all `persist` enabled state atoms. The object format returned here is the same as in the `initialState` option. * `activate()`: Emulate the `onActivate` event. By default, `startPlugin` already starts the plugin in activated state, and calling `activate` to test the `onActivate` event should be preceded by a `deactivate()` call first. * `deactivate()`: Emulates a user navigating away from the plugin. * `destroy()`: Emulates the plugin being cleaned up, for example because the plugin is disabled by the user, or because the device / client has disconnected. After calling `destroy` the current `runner` is unusable. * `triggerDeepLink(payload)`: Emulates a deepLink being triggered, and fires the `onDeepLink` event. * `triggerMenuEntry(label)`: Emulates the user clicking a menu entry in the Flipper main menu. * `flipperLib`: An object that exposed `jest.fn()` mocks for all built-in Flipper APIs that can be called by your plugin. So assertions can be made that the plugin did actually invoke those methods. For example: `expect(runner.flipperLib.createPaste).toBeCalledWith("test message")`. Currently supported mocks: `createPaste`, `enableMenuEntries`. The following members are available when using the `render...` variant rather than the `start...` variant: * `renderer`: This object can be used to query the DOM and further interact with it. It is provided by react-testing-library, and further documented [here](https://testing-library.com/docs/react-testing-library/api#render-result). * `act`: Use this function to wrap interactions with the plugin under test into a transaction, after which the DOM updates will be flushed by React. See also the [`act`](https://reactjs.org/docs/test-utils.html#act) documentation. The following members are only available for Client plugins: * `sendEvent(event, params)`: Emulates an event being sent by the client plugin. Will trigger the corresponding `onMessage` handler in the plugin. * `sendEvents({ method: string, params: object}[])`: Like `sendEvent`, but sends multiple events at once. * `onSend`: A `jest.fn()` that can be used to assert that `client.send()` was called by the plugin under test. For example `expect(runner.onSend).toBeCalledWith('currentLogs`, { since: 0})`. * `connect()`: Triggers the `onConnect()` event. (For non-background plugins `activate()` could as well be used for this). * `disconnect()`: Triggers the `onDisconnect()` event. (For non-background plugins `deactivate()` could as well be used for this). The following members are only available for Device plugins: * `sendLogEntry(logEntry)`: Emulates a log message arriving from the device. Triggers the `client.device.onLogEntry` listener.