Skip to content

Commit

Permalink
Merge pull request #2816 from udecode/feat/use-editor-selector
Browse files Browse the repository at this point in the history
Add `useEditorSelector` hook to subscribe to specific properties of `editor`
  • Loading branch information
12joan authored Dec 22, 2023
2 parents 96719fe + 775ca92 commit 35936d1
Show file tree
Hide file tree
Showing 67 changed files with 483 additions and 354 deletions.
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/plate-core@28.0.0

### 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/plate-comments@27.0.0
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

0 comments on commit 35936d1

Please sign in to comment.