React
import * as Hooks from '@dxos/app-framework/Hooks';A plugin’s components read capabilities, invoke operations and read settings through hooks. They work in any component under the app’s plugin manager, which every surface is.
Read capabilities from a component
Section titled “Read capabilities from a component”useCapability returns a singleton capability, and useCapabilities every contribution to a multi capability. Both
re-render when contributions change. useOptionalCapability returns undefined when no plugin provides it.
import * as import Capabilities
Capabilities from '@dxos/app-framework/Capabilities';import * as import Hooks
Hooks from '@dxos/app-framework/Hooks';
export const const SurfaceCount: () => React.JSX.Element
SurfaceCount = () => { // Every contribution to a multi capability, re-rendering when plugins add or // remove one. const const surfaces: any[]
surfaces = import Hooks
Hooks.const useCapabilities: <any>(interfaceDef: Capability.InterfaceDef<any>) => any[]
Hook to request capabilities from the plugin context.
useCapabilities(import Capabilities
Capabilities.const ReactSurface: Capability.MultiTag<any, "org.dxos.app-framework.capability.reactSurface">
Surface definitions that can be either React components or Web Components.
ReactSurface); return <JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span>{const surfaces: any[]
surfaces.Array<any>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length}</JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span>;};
Invoke operations from a component
Section titled “Invoke operations from a component”useOperation binds an operation to a callback; its argument maps the callback’s arguments to the operation’s input.
useOperationInvoker returns the invoker, whose invokePromise returns the operation’s output.
import { function useCallback<T extends Function>(callback: T, deps: React.DependencyList): T
useCallback will return a memoized version of the callback that only changes if one of the inputs
has changed.
useCallback } from 'react';import * as import Hooks
Hooks from '@dxos/app-framework/Hooks';import * as import LayoutOperation
LayoutOperation from '@dxos/app-toolkit/LayoutOperation';import * as import Button
Button from '@dxos/react-ui/Button';
export const const OpenButton: ({ path }: { path: string;}) => React.JSX.Element
OpenButton = ({ path: string
path }: { path: string
path: string }) => { const const open: () => void
open = import Hooks
Hooks.const useOperation: <[], { readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}>(operation: Definition<{ readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}, unknown, any>, map: () => { readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}, options?: InvokeOptions) => () => void
Binds an operation to a UI callback in one step: map turns the component's callback
arguments into the operation's input. The handler identity is stable across renders (the
mapper and options read through refs), so it replaces per-handler useCallback boilerplate.
useOperation(import LayoutOperation
LayoutOperation.const Open: Definition<{ readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}, readonly string[], Capability.Service>
Open, () => ({ subject: readonly string[]
subject: [path: string
path], })); return <import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick={() => const open: () => void
open()}>Open</import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root>;};
export const const OpenTwice: ({ path }: { path: string;}) => React.JSX.Element
OpenTwice = ({ path: string
path }: { path: string
path: string }) => { const { const invokePromise: <I, O>(op: Definition<I, O>, ...args: void extends I ? [input?: I, options?: InvokeOptions] : [input: I, options?: InvokeOptions]) => Promise<{ data?: O; error?: Error;}>
invokePromise } = import Hooks
Hooks.const useOperationInvoker: () => Capabilities.OperationInvoker
Hook to get the operation invoker capability.
useOperationInvoker(); const const handleClick: () => Promise<void>
handleClick = useCallback<() => Promise<void>>(callback: () => Promise<void>, deps: React.DependencyList): () => Promise<void>
useCallback will return a memoized version of the callback that only changes if one of the inputs
has changed.
useCallback(async () => { await const invokePromise: <{ readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}, readonly string[]>(op: Definition<{ readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}, readonly string[], any>, input: { readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}, options?: InvokeOptions | undefined) => Promise<...>
invokePromise(import LayoutOperation
LayoutOperation.const Open: Definition<{ readonly subject: readonly string[]; readonly variant?: string | undefined; readonly name?: string | undefined; readonly workspace?: string | undefined; readonly scrollIntoView?: boolean | undefined; readonly focus?: "content" | boolean | undefined; readonly navigation?: "immediate" | "validate" | undefined; readonly pivotId?: string | undefined; readonly disposition?: "add" | "auto" | "detail" | "solo" | undefined; readonly modifiers?: { readonly shift?: boolean | undefined; } | undefined;}, readonly string[], Capability.Service>
Open, { subject: readonly string[]
subject: [path: string
path] }); }, [const invokePromise: <I, O>(op: Definition<I, O>, ...args: void extends I ? [input?: I, options?: InvokeOptions] : [input: I, options?: InvokeOptions]) => Promise<{ data?: O; error?: Error;}>
invokePromise, path: string
path]); return <import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick={const handleClick: () => Promise<void>
handleClick}>Open</import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root>;};
Read plugin settings and translations
Section titled “Read plugin settings and translations”useAtomCapability reads an atom capability, such as a plugin’s settings, and re-renders when it changes.
useAtomCapabilityState also returns a setter. useTranslation from @dxos/react-ui/Hooks takes the plugin’s
namespace.
import * as import Hooks
Hooks from '@dxos/app-framework/Hooks';import * as import Button
Button from '@dxos/react-ui/Button';import * as import UiHooks
UiHooks from '@dxos/react-ui/Hooks';
export const const CompactToggle: () => React.JSX.Element
CompactToggle = () => { const [const settings: { compact: boolean;}
settings, const setSettings: (fn: (current: { compact: boolean;}) => { compact: boolean;}) => void
setSettings] = import Hooks
Hooks.const useAtomCapabilityState: <{ compact: boolean;}>(atomCapability: Capability.InterfaceDef<Writable<{ compact: boolean;}, { compact: boolean;}>>) => [{ compact: boolean;}, (fn: (current: { compact: boolean;}) => { compact: boolean;}) => void]
Hook to get value and updater for an atom capability.
Returns [currentValue, updateFn] similar to useState.
useAtomCapabilityState(const Settings: Capability.Tag<Writable<{ compact: boolean;}, { compact: boolean;}>, "com.example.plugin.notes.capability.settings">
Settings); const { const t: TFunction<Namespace<string>, KeyPrefix<Namespace<string>>>
t } = import UiHooks
UiHooks.function useTranslation(ns?: string | string[] | readonly string[] | undefined, options?: UseTranslationOptions<KeyPrefix<Namespace<string>>> | undefined): { toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n | undefined; push(...items: (boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n)[]): number; concat(...items: ConcatArray<boolean | TFunction<...> | import("i18next").i18n>[]): (boolean | TFunction<...> | import("i18next").i18n)[]; concat(...items: (boolean | ConcatArray<boolean | TFunction<...> | import("i18next").i18n> | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[]; join(separator?: string): string; reverse(): (boolean | TFunction<...> | import("i18next").i18n)[]; shift(): boolean | TFunction<...> | import("i18next").i18n | undefined; slice(start?: number, end?: number): (boolean | TFunction<...> | import("i18next").i18n)[]; sort(compareFn?: ((a: boolean | TFunction<...> | import("i18next").i18n, b: boolean | TFunction<...> | import("i18next").i18n) => number) | undefined): UseTranslationResponse<...>; splice(start: number, deleteCount?: number): (boolean | TFunction<...> | import("i18next").i18n)[]; splice(start: number, deleteCount: number, ...items: (boolean | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[]; unshift(...items: (boolean | TFunction<...> | import("i18next").i18n)[]): number; indexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number; lastIndexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number; every<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => void, thisArg?: any): void; map<U>(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, thisArg?: any): U[]; filter<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): (boolean | TFunction<...> | import("i18next").i18n)[]; reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n, initialValue: boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduce<U>(callbackfn: (previousValue: U, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | ...export useTranslation
useTranslation('com.example.plugin.notes'); return ( <import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick={() => const setSettings: (fn: (current: { compact: boolean;}) => { compact: boolean;}) => void
setSettings((current: { compact: boolean;}
current) => ({ ...current: { compact: boolean;}
current, compact: boolean
compact: !current: { compact: boolean;}
current.compact: boolean
compact })) } > {const settings: { compact: boolean;}
settings.compact: boolean
compact ? const t: TFunction<"expand.label", ((string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<AwaitedReactNode>) & (string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | Iterable<ReactI18NextChild>)) | null | undefined, StringMap>(key: "expand.label" | "expand.label"[], options?: string | ... 1 more ... | undefined) => ((string | ... 6 more ... | Promise<AwaitedReactNode>) & (string | ... 7 more ... | Iterable<...>)) | ... 1 more ... | undefined (+1 overload)
t('expand.label') : const t: TFunction<"compact.label", ((string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<AwaitedReactNode>) & (string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | Iterable<ReactI18NextChild>)) | null | undefined, StringMap>(key: "compact.label" | "compact.label"[], options?: string | ... 1 more ... | undefined) => ((string | ... 6 more ... | Promise<AwaitedReactNode>) & (string | ... 7 more ... | Iterable<...>)) | ... 1 more ... | undefined (+1 overload)
t('compact.label')} </import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root> );};
@dxos/app-toolkit/Hooks adds hooks for app state, such as useActiveSpace, useAppGraph and useLayout.
The reactivity rules
Section titled “The reactivity rules”- Reading an ECHO object’s field in render does not subscribe the component. Use
useObjectfor a snapshot that does, anduseQueryfor a list. See the ECHO React guide. - Write objects inside
Obj.update, from an event handler or an operation, never during render. - A component never assumes it is the only view of an object. The same object can be open in another plank, on another device, or in the assistant’s hands.
import { function useCallback<T extends Function>(callback: T, deps: React.DependencyList): T
useCallback will return a memoized version of the callback that only changes if one of the inputs
has changed.
useCallback } from 'react';import * as import ToolkitHooks
ToolkitHooks from '@dxos/app-toolkit/Hooks';import { import Obj
Obj } from '@dxos/echo';import { const useObject: { <T extends Obj.Unknown>(ref: Ref<T>): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>]; <T extends Obj.Unknown>(ref: Ref<T> | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>]; <T extends Obj.Unknown>(ref: Ref<T> | undefined, options: LoadOptions): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>]; <T extends Obj.Unknown>(obj: T): [Obj.Snapshot<T>, ObjectUpdateCallback<T>]; <T extends Obj.Unknown>(obj: T | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>]; <T extends Obj.Unknown>(objOrRef: T | Ref<T> | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>]; <T extends Obj.Unknown, K extends keyof T>(obj: T, property: K): [T[K], ObjectPropUpdateCallback<T[K]>]; <T extends Obj.Unknown, K extends keyof T>(obj: T | undefined, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>]; <T, K extends keyof T>(ref: Ref<T>, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>]; <T, K extends keyof T>(ref: Ref<T> | undefined, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>];}
useObject } from '@dxos/echo-react';import * as import Button
Button from '@dxos/react-ui/Button';
export const const NoteTitle: ({ note }: { note: Note;}) => React.JSX.Element
NoteTitle = ({ note: Note
note }: { note: Note
note: class Note
Note }) => { // Reading `note.title` in render does not subscribe. `useObject` returns a // snapshot that does. const [const snapshot: Obj.Snapshot<Note>
snapshot] = useObject<Note>(obj: Note): [Obj.Snapshot<Note>, ObjectUpdateCallback<Note>] (+9 overloads)
Hook to subscribe to an entire Echo object.
Returns a snapshot of the current object value and automatically re-renders when the object changes.
useObject(note: Note
note); const const space: Space | undefined
space = import ToolkitHooks
ToolkitHooks.const useActiveSpace: () => import("@dxos/client-protocol").Space | undefined
useActiveSpace(); const const rename: () => Note
rename = useCallback<() => Note>(callback: () => Note, deps: React.DependencyList): () => Note
useCallback will return a memoized version of the callback that only changes if one of the inputs
has changed.
useCallback( () => import Obj
Obj.const update: <Note>(obj: Note, callback: ChangeCallback<Note>) => Note
Perform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification
when the callback completes. Direct mutations outside of Obj.update will throw
an error for echo objects.
This function also works with nested objects within echo objects (e.g., Template structs)
that are reactive at runtime.
update(note: Note
note, (note: { title?: string | undefined; id: EntityId; "~@dxos/echo/Kind": EntityKind.Object;}
note) => (note: { title?: string | undefined; id: EntityId; "~@dxos/echo/Kind": EntityKind.Object;}
note.title?: string | undefined
title = 'Renamed')), [note: Note
note], ); return ( <import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick={const rename: () => Note
rename} data-testid: SpaceId | undefined
data-testid={const space: Space | undefined
space?.Space.id: SpaceId | undefined
Unique space identifier.
id}> {const snapshot: Obj.Snapshot<Note>
snapshot.title?: string | undefined
title} </import Button
Button.const Root: React.ForwardRefExoticComponent<Omit<Button.RootProps, "className"> & { classNames?: ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "children" | "style" | "role"> & React.RefAttributes<HTMLButtonElement>>export Root
Root> );};
Related
Section titled “Related”| Page | What it covers |
|---|---|
| Surface | Where these components render. |
| Operation | Defining the operations a component invokes. |