Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useEditorSelector hook to subscribe to specific properties of editor #2816

Merged
merged 14 commits into from
Dec 22, 2023
Merged
5 changes: 5 additions & 0 deletions .changeset/fn-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-comments': patch
---

- Remove `{ fn: ... }` workaround for jotai stores that contain functions
5 changes: 5 additions & 0 deletions .changeset/fn-resizable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-resizable': patch
---

- Remove `{ fn: ... }` workaround for jotai stores that contain functions
5 changes: 5 additions & 0 deletions .changeset/patch-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-alignment': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-emoji.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-emoji': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-floating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-floating': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-font.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-font': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-indent-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-indent-list': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-line-height.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-line-height': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-link': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-list': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-tabbable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-tabbable': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-table': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
5 changes: 5 additions & 0 deletions .changeset/patch-utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-utils': patch
---

- Replace `useEdtiorState` with `useEditorSelector`
8 changes: 8 additions & 0 deletions .changeset/spicy-bobcats-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@udecode/plate-core': major
---

- Upgrade to `[email protected]`
- Add `useEditorSelector` hook to only re-render when a specific property of `editor` changes
- Remove `{ fn: ... }` workaround for jotai stores that contain functions
- Breaking change: `usePlateSelectors`, `usePlateActions` and `usePlateStates` no longer accept generic type arguments. If custom types are required, cast the resulting values at the point of use, or use hooks like `useEditorRef` that still provide generics.
8 changes: 8 additions & 0 deletions BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# 28.0.0

## @udecode/[email protected]

### Major Changes

- `usePlateSelectors`, `usePlateActions` and `usePlateStates` no longer accept generic type arguments. If custom types are required, cast the resulting values at the point of use, or use hooks like `useEditorRef` that still provide generics.

# 27.0.0

## @udecode/[email protected]
Expand Down
51 changes: 29 additions & 22 deletions apps/www/content/docs/accessing-editor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,38 @@ const createMyPlugin = createPluginFactory({

## From a Child of Plate

Use the **`useEditorRef`** or **`useEditorState`** hooks.

Internally, **`useEditorState`** is a wrapper for **`useEditorRef`**. The only difference is that **`useEditorState`** causes React to re-render whenever the **`editor`** state changes, whereas **`useEditorRef`** does not cause a re-render. Since **`editor`** is mutable and is updated by reference, **`useEditorRef`** will be sufficient (and more efficient) in most situations.
Use the **`useEditorRef`**, **`useEditorSelector`** or **`useEditorState`** hooks. Which of these hooks you should use depends on when you want your component to re-render in response to changes to **`editor`**.

- **`useEditorRef`** - Use a reference to **`editor`** that almost never gets replaced. **This should be the default choice.**
- Since **`editor`** is a mutable object that gets updated by reference, **`useEditorRef`** is always sufficient for accessing the **`editor`** inside callbacks.
- **`useEditorRef`** will almost never cause your component to re-render, so it's the best choice for performance.
- **`useEditorSelector`** - Subscribe to a specific selector based on **`editor`**. **This is the most performant option for subscribing to state changes.**
- Example usage: `const hasSelection = useEditorSelector((editor) => !!editor.selection, []);`
- When you want your component to re-render in response to a specific change that you're interested in, you can use **`useEditorSelector`** to access the relevant property.
- The selector function is called every time the **`editor`** changes (i.e. on every keystroke or selection change), but the component only re-renders when the return value changes.
- For this to work properly, you should make sure that the return value can be compared using `===`. In most cases, this means returning a primitive value, like a number, string or boolean.
- You can provide a custom **`equalityFn`** in the options to **`useEditorSelector`** for cases where `===` isn't sufficient.
- If the selector function depends on any locally scoped variables, you should include these in the dependency list.
- **`useEditorState`** - Re-render every time the **`editor`** changes.
- Using **`useEditorState`** will cause your component to re-render every time the user presses a key or changes the selection.
- This may cause performance issues for large documents, or when re-rendering is particularly expensive.

You can call these hooks from any React component that is rendered as a descendant of the **`Plate`** component, including [Plugin Components](/docs/plugin-components).

```tsx
const Toolbar = () => {
const boldActive = useEditorSelector((editor) => isMarkActive(editor, MARK_BOLD), []);
// ...
};

const Editor = () => (
<Plate>
<Toolbar />
<PlateContent />
</Plate>
);
```

```tsx showLineNumbers {6}
const ParagraphElement = ({
className,
Expand Down Expand Up @@ -118,25 +144,6 @@ export default () => (
as when the editor is reset.
</Callout>

## From a Sibling of Plate

Wrap **`PlateContent`** and the sibling in **`Plate`**, and then use **`useEditorRef`** or **`useEditorState`** from within the sibling.

```tsx showLineNumbers {2,8,11}
const Toolbar = () => {
const editor = useEditorState();
// Do something with editor
// ...
};

const Editor = () => (
<Plate>
<Toolbar />
<PlateContent />
</Plate>
);
```

## From an Ancestor

If you need to access the **`editor`** instance from an ancestor of **`PlateContent`**, wrapping the relevant components in a **`Plate`** is the preferred solution. If this is not an option, you can instead use the **`editorRef`** prop to pass a reference to the **`editor`** instance up the React component tree to where it is needed.
Expand Down
49 changes: 35 additions & 14 deletions apps/www/content/docs/api/core.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,6 @@ Plugin attributes to override by plugin key.
An array of plugins with overridden components or attributes.
</APIReturns>

<APIReturns>
An object containing the following properties:
<APIItem name="pressed" type="boolean">
A boolean indicating whether the `nodeType` mark is active in the current
selection.
</APIItem>
<APIItem name="nodeType" type="string">
The type of the node.
</APIItem>
<APIItem name="clear" type="string | string[]">
Type or types of the node to clear.
</APIItem>
</APIReturns>

### createAtomStore

Creates an atom store from an initial value. Each property of the initial value will have a getter and setter.
Expand Down Expand Up @@ -344,6 +330,40 @@ A `PlateEditor` object, which is the Slate editor.

</APIReturns>

### useEditorSelector

Subscribe to a specific property of the editor.

- Calls the selector function on editor change.
- Re-renders when the result of the selector changes.
- Should be used inside `Plate`.

<APIParameters>
<APIItem name="selector" type="(editor: PlateEditor<V>, prev?: T) => T">
The selector function.
</APIItem>

<APIItem name="deps" type="DependencyList">
The dependency list for the selector function.
</APIItem>

<APIItem name="options" type="UseEditorSelectorOptions<T>" optional>
<APISubList>
<APISubListItem parent="options" name="id" type="PlateId" optional>
The ID of the plate editor. Useful only when nesting editors. Default is using the closest editor id.
</APISubListItem>

<APISubListItem parent="options" name="equalityFn" type="(a: T, b: T) => boolean" optional>
Equality function to determine whether the result of the selector function has changed. Default is `(a, b) => a === b`.
</APISubListItem>
</APISubList>
</APIItem>
</APIParameters>

<APIReturns>
The return value of the selector function.
</APIReturns>

### useEditorState

Get the Slate editor reference with re-rendering.
Expand All @@ -352,6 +372,7 @@ Get the Slate editor reference with re-rendering.
- Supports nested editors.
- Should be used inside `Plate`.
- Note the reference does not change when the editor changes.
- If performance is a concern, `useEditorSelector` should be used instead.

<APIParameters>
<APIItem name="id" type="PlateId" optional>
Expand Down
28 changes: 4 additions & 24 deletions apps/www/content/docs/api/core/store.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,40 +84,20 @@ Version incremented when calling `redecorate`. This is a dependency of the `deco

</APIItem>

<APIItem name="onChange" type="object">
<APISubList>
<APISubListItem parent="onChange" name="fn" type="function">
<APIItem name="onChange" type="function">
- See [`onChange`](/docs/api/core/plate#slate-onchange).
</APISubListItem>
</APISubList>

</APIItem>

<APIItem name="decorate" type="object" >
<APISubList>
<APISubListItem parent="decorate" name="fn" type="function" >
<APIItem name="decorate" type="function" >
- See [`decorate`](/docs/api/core/plate#editable-decorate).
</APISubListItem>
</APISubList>

</APIItem>

<APIItem name="renderElement" type="object" >
<APISubList>
<APISubListItem parent="renderElement" name="fn" type="function" >
<APIItem name="renderElement" type="function" >
- See [`renderElement`](/docs/api/core/plate#editable-renderelement).
</APISubListItem>
</APISubList>

</APIItem>

<APIItem name="renderLeaf" type="object" >
<APISubList>
<APISubListItem parent="renderLeaf" name="fn" type="function" >
<APIItem name="renderLeaf" type="function" >
- See [`renderLeaf`](/docs/api/core/plate#editable-renderleaf).
</APISubListItem>
</APISubList>

</APIItem>
</APIAttributes>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import {
focusEditor,
insertEmptyElement,
useEditorState,
useEditorRef,
} from '@udecode/plate-common';
import { ELEMENT_EXCALIDRAW } from '@udecode/plate-excalidraw';
import {
Expand Down Expand Up @@ -170,7 +170,7 @@ const items = [
];

export function PlaygroundInsertDropdownMenu(props: DropdownMenuProps) {
const editor = useEditorState();
const editor = useEditorRef();
const openState = useOpenState();

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
import {
focusEditor,
useEditorReadOnly,
useEditorState,
useEditorRef,
usePlateStore,
} from '@udecode/plate-common';

Expand All @@ -19,7 +19,7 @@ import {
import { ToolbarButton } from '@/registry/default/plate-ui/toolbar';

export function PlaygroundModeDropdownMenu(props: DropdownMenuProps) {
const editor = useEditorState();
const editor = useEditorRef();
const setReadOnly = usePlateStore().set.readOnly();
const readOnly = useEditorReadOnly();
const openState = useOpenState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
collapseSelection,
focusEditor,
toggleMark,
useEditorState,
useEditorRef,
} from '@udecode/plate-common';
import { MARK_HIGHLIGHT } from '@udecode/plate-highlight';
import { MARK_KBD } from '@udecode/plate-kbd';
Expand All @@ -21,7 +21,7 @@ import {
import { ToolbarButton } from '@/registry/default/plate-ui/toolbar';

export function PlaygroundMoreDropdownMenu(props: DropdownMenuProps) {
const editor = useEditorState();
const editor = useEditorRef();
const openState = useOpenState();

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import {
findNode,
focusEditor,
isBlock,
isCollapsed,
isSelectionExpanded,
TElement,
toggleNodeType,
useEditorState,
useEditorRef,
useEditorSelector,
} from '@udecode/plate-common';
import {
ELEMENT_H1,
Expand Down Expand Up @@ -105,20 +106,26 @@ const items = [
const defaultItem = items.find((item) => item.value === ELEMENT_PARAGRAPH)!;

export function PlaygroundTurnIntoDropdownMenu(props: DropdownMenuProps) {
const editor = useEditorState();
const editor = useEditorRef();
const openState = useOpenState();

let value: string = ELEMENT_PARAGRAPH;
if (isCollapsed(editor?.selection)) {
const entry = findNode<TElement>(editor!, {
match: (n) => isBlock(editor, n),
});
if (entry) {
value =
items.find((item) => item.value === entry[0].type)?.value ??
ELEMENT_PARAGRAPH;
// eslint-disable-next-line @typescript-eslint/no-shadow
const value: string = useEditorSelector((editor) => {
if (!isSelectionExpanded(editor)) {
const entry = findNode<TElement>(editor!, {
match: (n) => isBlock(editor, n),
});

if (entry) {
return (
items.find((item) => item.value === entry[0].type)?.value ??
ELEMENT_PARAGRAPH
);
}
}
}

return ELEMENT_PARAGRAPH;
}, []);

const selectedItem =
items.find((item) => item.value === value) ?? defaultItem;
Expand Down
Loading