diff --git a/.changeset/fn-comments.md b/.changeset/fn-comments.md new file mode 100644 index 0000000000..97a7381597 --- /dev/null +++ b/.changeset/fn-comments.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-comments': patch +--- + +- Remove `{ fn: ... }` workaround for jotai stores that contain functions diff --git a/.changeset/fn-resizable.md b/.changeset/fn-resizable.md new file mode 100644 index 0000000000..94d303b80a --- /dev/null +++ b/.changeset/fn-resizable.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-resizable': patch +--- + +- Remove `{ fn: ... }` workaround for jotai stores that contain functions diff --git a/.changeset/patch-alignment.md b/.changeset/patch-alignment.md new file mode 100644 index 0000000000..9e2256ccbf --- /dev/null +++ b/.changeset/patch-alignment.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-alignment': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-emoji.md b/.changeset/patch-emoji.md new file mode 100644 index 0000000000..e7c0d9cab0 --- /dev/null +++ b/.changeset/patch-emoji.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-emoji': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-floating.md b/.changeset/patch-floating.md new file mode 100644 index 0000000000..11bcb57d8a --- /dev/null +++ b/.changeset/patch-floating.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-floating': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-font.md b/.changeset/patch-font.md new file mode 100644 index 0000000000..b6c75904f6 --- /dev/null +++ b/.changeset/patch-font.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-font': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-indent-list.md b/.changeset/patch-indent-list.md new file mode 100644 index 0000000000..17ad244df8 --- /dev/null +++ b/.changeset/patch-indent-list.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-indent-list': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-line-height.md b/.changeset/patch-line-height.md new file mode 100644 index 0000000000..6b8cd4bf42 --- /dev/null +++ b/.changeset/patch-line-height.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-line-height': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-link.md b/.changeset/patch-link.md new file mode 100644 index 0000000000..7e646a3024 --- /dev/null +++ b/.changeset/patch-link.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-link': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-list.md b/.changeset/patch-list.md new file mode 100644 index 0000000000..a031457f93 --- /dev/null +++ b/.changeset/patch-list.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-list': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-tabbable.md b/.changeset/patch-tabbable.md new file mode 100644 index 0000000000..b144d50daa --- /dev/null +++ b/.changeset/patch-tabbable.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-tabbable': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-table.md b/.changeset/patch-table.md new file mode 100644 index 0000000000..d685a0de5c --- /dev/null +++ b/.changeset/patch-table.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-table': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/patch-utils.md b/.changeset/patch-utils.md new file mode 100644 index 0000000000..3a0fc9b14a --- /dev/null +++ b/.changeset/patch-utils.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-utils': patch +--- + +- Replace `useEdtiorState` with `useEditorSelector` diff --git a/.changeset/spicy-bobcats-taste.md b/.changeset/spicy-bobcats-taste.md new file mode 100644 index 0000000000..ce9daa2dc2 --- /dev/null +++ b/.changeset/spicy-bobcats-taste.md @@ -0,0 +1,8 @@ +--- +'@udecode/plate-core': major +--- + +- Upgrade to `jotai-x@1.1.0` +- 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. diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index c91044005d..765b02e765 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -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 diff --git a/apps/www/content/docs/accessing-editor.mdx b/apps/www/content/docs/accessing-editor.mdx index bdf1112aa5..f71c3bf6df 100644 --- a/apps/www/content/docs/accessing-editor.mdx +++ b/apps/www/content/docs/accessing-editor.mdx @@ -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 = () => ( + + + + +); +``` + ```tsx showLineNumbers {6} const ParagraphElement = ({ className, @@ -118,25 +144,6 @@ export default () => ( as when the editor is reset. -## 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 = () => ( - - - - -); -``` - ## 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. diff --git a/apps/www/content/docs/api/core.mdx b/apps/www/content/docs/api/core.mdx index 20a774a984..b5954fc95c 100644 --- a/apps/www/content/docs/api/core.mdx +++ b/apps/www/content/docs/api/core.mdx @@ -31,20 +31,6 @@ Plugin attributes to override by plugin key. An array of plugins with overridden components or attributes. - - An object containing the following properties: - - A boolean indicating whether the `nodeType` mark is active in the current - selection. - - - The type of the node. - - - Type or types of the node to clear. - - - ### createAtomStore Creates an atom store from an initial value. Each property of the initial value will have a getter and setter. @@ -344,6 +330,40 @@ A `PlateEditor` object, which is the Slate editor. +### 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`. + + + + The selector function. + + + + The dependency list for the selector function. + + + + + + The ID of the plate editor. Useful only when nesting editors. Default is using the closest editor id. + + + + Equality function to determine whether the result of the selector function has changed. Default is `(a, b) => a === b`. + + + + + + + The return value of the selector function. + + ### useEditorState Get the Slate editor reference with re-rendering. @@ -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. diff --git a/apps/www/content/docs/api/core/store.mdx b/apps/www/content/docs/api/core/store.mdx index 94181cb543..1c5b146e1e 100644 --- a/apps/www/content/docs/api/core/store.mdx +++ b/apps/www/content/docs/api/core/store.mdx @@ -84,40 +84,20 @@ Version incremented when calling `redecorate`. This is a dependency of the `deco - - - + - See [`onChange`](/docs/api/core/plate#slate-onchange). - - - - - - + - See [`decorate`](/docs/api/core/plate#editable-decorate). - - - - - - + - See [`renderElement`](/docs/api/core/plate#editable-renderelement). - - - - - - + - See [`renderLeaf`](/docs/api/core/plate#editable-renderleaf). - - - diff --git a/apps/www/src/components/plate-ui/playground-insert-dropdown-menu.tsx b/apps/www/src/components/plate-ui/playground-insert-dropdown-menu.tsx index 60a0c65aca..4b44e881e7 100644 --- a/apps/www/src/components/plate-ui/playground-insert-dropdown-menu.tsx +++ b/apps/www/src/components/plate-ui/playground-insert-dropdown-menu.tsx @@ -10,7 +10,7 @@ import { import { focusEditor, insertEmptyElement, - useEditorState, + useEditorRef, } from '@udecode/plate-common'; import { ELEMENT_EXCALIDRAW } from '@udecode/plate-excalidraw'; import { @@ -170,7 +170,7 @@ const items = [ ]; export function PlaygroundInsertDropdownMenu(props: DropdownMenuProps) { - const editor = useEditorState(); + const editor = useEditorRef(); const openState = useOpenState(); return ( diff --git a/apps/www/src/components/plate-ui/playground-mode-dropdown-menu.tsx b/apps/www/src/components/plate-ui/playground-mode-dropdown-menu.tsx index 73eef1456f..bae54b9afe 100644 --- a/apps/www/src/components/plate-ui/playground-mode-dropdown-menu.tsx +++ b/apps/www/src/components/plate-ui/playground-mode-dropdown-menu.tsx @@ -3,7 +3,7 @@ import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; import { focusEditor, useEditorReadOnly, - useEditorState, + useEditorRef, usePlateStore, } from '@udecode/plate-common'; @@ -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(); diff --git a/apps/www/src/components/plate-ui/playground-more-dropdown-menu.tsx b/apps/www/src/components/plate-ui/playground-more-dropdown-menu.tsx index 38f375115d..5427cb36ce 100644 --- a/apps/www/src/components/plate-ui/playground-more-dropdown-menu.tsx +++ b/apps/www/src/components/plate-ui/playground-more-dropdown-menu.tsx @@ -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'; @@ -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 ( diff --git a/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx b/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx index e40aa3df2c..a1e041bd4d 100644 --- a/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx +++ b/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx @@ -6,10 +6,11 @@ import { findNode, focusEditor, isBlock, - isCollapsed, + isSelectionExpanded, TElement, toggleNodeType, - useEditorState, + useEditorRef, + useEditorSelector, } from '@udecode/plate-common'; import { ELEMENT_H1, @@ -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(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(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; diff --git a/apps/www/src/registry/default/plate-ui/combobox.tsx b/apps/www/src/registry/default/plate-ui/combobox.tsx index afae2a97ff..57a158e524 100644 --- a/apps/www/src/registry/default/plate-ui/combobox.tsx +++ b/apps/www/src/registry/default/plate-ui/combobox.tsx @@ -17,7 +17,12 @@ import { useComboboxItem, useComboboxSelectors, } from '@udecode/plate-combobox'; -import { useEditorState, useEventEditorSelectors } from '@udecode/plate-common'; +import { + useEditorRef, + useEditorSelector, + useEventEditorSelectors, + usePlateSelectors, +} from '@udecode/plate-common'; import { createVirtualRef } from '@udecode/plate-floating'; import { cn } from '@/lib/utils'; @@ -52,7 +57,7 @@ export function ComboboxContent( onRenderItem, } = props; - const editor = useEditorState(); + const editor = useEditorRef(); const filteredItems = useComboboxSelectors.filteredItems() as TComboboxItem[]; @@ -118,7 +123,11 @@ export function Combobox({ const focusedEditorId = useEventEditorSelectors.focus?.(); const combobox = useComboboxControls(); const activeId = useComboboxSelectors.activeId(); - const editor = useEditorState(); + const selectionDefined = useEditorSelector( + (editor) => !!editor.selection, + [] + ); + const editorId = usePlateSelectors().id(); useEffect(() => { comboboxActions.setComboboxById({ @@ -144,8 +153,8 @@ export function Combobox({ if ( !combobox || - !editor.selection || - focusedEditorId !== editor.id || + !selectionDefined || + focusedEditorId !== editorId || activeId !== id || disabled ) { diff --git a/apps/www/src/registry/default/plate-ui/insert-dropdown-menu.tsx b/apps/www/src/registry/default/plate-ui/insert-dropdown-menu.tsx index 02c221dc8e..fd222cccf0 100644 --- a/apps/www/src/registry/default/plate-ui/insert-dropdown-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/insert-dropdown-menu.tsx @@ -6,7 +6,7 @@ import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; import { focusEditor, insertEmptyElement, - useEditorState, + useEditorRef, } from '@udecode/plate-common'; import { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; @@ -127,7 +127,7 @@ const items = [ ]; export function InsertDropdownMenu(props: DropdownMenuProps) { - const editor = useEditorState(); + const editor = useEditorRef(); const openState = useOpenState(); return ( diff --git a/apps/www/src/registry/default/plate-ui/media-popover.tsx b/apps/www/src/registry/default/plate-ui/media-popover.tsx index d3422d14c9..5e9396beac 100644 --- a/apps/www/src/registry/default/plate-ui/media-popover.tsx +++ b/apps/www/src/registry/default/plate-ui/media-popover.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { - isCollapsed, - useEditorState, + isSelectionExpanded, + useEditorSelector, useElement, useRemoveNodeButton, } from '@udecode/plate-common'; @@ -27,9 +27,12 @@ export interface MediaPopoverProps { export function MediaPopover({ pluginKey, children }: MediaPopoverProps) { const readOnly = useReadOnly(); const selected = useSelected(); - const editor = useEditorState(); - const isOpen = !readOnly && selected && isCollapsed(editor.selection); + const selectionCollapsed = useEditorSelector( + (editor) => !isSelectionExpanded(editor), + [] + ); + const isOpen = !readOnly && selected && selectionCollapsed; const isEditing = useFloatingMediaSelectors().isEditing(); useEffect(() => { diff --git a/apps/www/src/registry/default/plate-ui/mode-dropdown-menu.tsx b/apps/www/src/registry/default/plate-ui/mode-dropdown-menu.tsx index a931815f68..4136cdfc7c 100644 --- a/apps/www/src/registry/default/plate-ui/mode-dropdown-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/mode-dropdown-menu.tsx @@ -3,7 +3,7 @@ import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; import { focusEditor, useEditorReadOnly, - useEditorState, + useEditorRef, usePlateStore, } from '@udecode/plate-common'; @@ -20,7 +20,7 @@ import { import { ToolbarButton } from './toolbar'; export function ModeDropdownMenu(props: DropdownMenuProps) { - const editor = useEditorState(); + const editor = useEditorRef(); const setReadOnly = usePlateStore().set.readOnly(); const readOnly = useEditorReadOnly(); const openState = useOpenState(); diff --git a/apps/www/src/registry/default/plate-ui/more-dropdown-menu.tsx b/apps/www/src/registry/default/plate-ui/more-dropdown-menu.tsx index 60f3b3bfd8..83853bf94d 100644 --- a/apps/www/src/registry/default/plate-ui/more-dropdown-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/more-dropdown-menu.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; import { MARK_SUBSCRIPT, MARK_SUPERSCRIPT } from '@udecode/plate-basic-marks'; -import { focusEditor, toggleMark, useEditorState } from '@udecode/plate-common'; +import { focusEditor, toggleMark, useEditorRef } from '@udecode/plate-common'; import { Icons } from '@/components/icons'; @@ -15,7 +15,7 @@ import { import { ToolbarButton } from './toolbar'; export function MoreDropdownMenu(props: DropdownMenuProps) { - const editor = useEditorState(); + const editor = useEditorRef(); const openState = useOpenState(); return ( diff --git a/apps/www/src/registry/default/plate-ui/table-dropdown-menu.tsx b/apps/www/src/registry/default/plate-ui/table-dropdown-menu.tsx index 99f085f8c3..cc5de0e78e 100644 --- a/apps/www/src/registry/default/plate-ui/table-dropdown-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/table-dropdown-menu.tsx @@ -1,6 +1,11 @@ import React from 'react'; import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; -import { focusEditor, someNode, useEditorState } from '@udecode/plate-common'; +import { + focusEditor, + someNode, + useEditorRef, + useEditorSelector, +} from '@udecode/plate-common'; import { deleteColumn, deleteRow, @@ -26,11 +31,13 @@ import { import { ToolbarButton } from './toolbar'; export function TableDropdownMenu(props: DropdownMenuProps) { - const editor = useEditorState(); + const editor = useEditorRef(); - const tableSelected = someNode(editor, { - match: { type: ELEMENT_TABLE }, - }); + const tableSelected = useEditorSelector( + // eslint-disable-next-line @typescript-eslint/no-shadow + (editor) => someNode(editor, { match: { type: ELEMENT_TABLE } }), + [] + ); const openState = useOpenState(); diff --git a/apps/www/src/registry/default/plate-ui/table-element.tsx b/apps/www/src/registry/default/plate-ui/table-element.tsx index b016d57db9..a7b0130694 100644 --- a/apps/www/src/registry/default/plate-ui/table-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-element.tsx @@ -2,10 +2,11 @@ import React, { forwardRef } from 'react'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import { PopoverAnchor, PopoverContentProps } from '@radix-ui/react-popover'; import { - isCollapsed, + isSelectionExpanded, PlateElement, PlateElementProps, - useEditorState, + useEditorRef, + useEditorSelector, useElement, useRemoveNodeButton, } from '@udecode/plate-common'; @@ -116,9 +117,14 @@ const TableFloatingToolbar = React.forwardRef< const readOnly = useReadOnly(); const selected = useSelected(); - const editor = useEditorState(); + const editor = useEditorRef(); - const collapsed = !readOnly && selected && isCollapsed(editor.selection); + const selectionCollapsed = useEditorSelector( + // eslint-disable-next-line @typescript-eslint/no-shadow + (editor) => !isSelectionExpanded(editor), + [] + ); + const collapsed = !readOnly && selected && selectionCollapsed; const open = !readOnly && selected; const { canMerge, canUnmerge } = useTableMergeState(); diff --git a/apps/www/src/registry/default/plate-ui/turn-into-dropdown-menu.tsx b/apps/www/src/registry/default/plate-ui/turn-into-dropdown-menu.tsx index 16818e7ee4..51b843cb1e 100644 --- a/apps/www/src/registry/default/plate-ui/turn-into-dropdown-menu.tsx +++ b/apps/www/src/registry/default/plate-ui/turn-into-dropdown-menu.tsx @@ -9,7 +9,8 @@ import { isCollapsed, TElement, toggleNodeType, - useEditorState, + useEditorRef, + useEditorSelector, } from '@udecode/plate-common'; import { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; @@ -75,20 +76,26 @@ const items = [ const defaultItem = items.find((item) => item.value === ELEMENT_PARAGRAPH)!; export function TurnIntoDropdownMenu(props: DropdownMenuProps) { - const editor = useEditorState(); + const editor = useEditorRef(); const openState = useOpenState(); - let value: string = ELEMENT_PARAGRAPH; - if (isCollapsed(editor?.selection)) { - const entry = findNode(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 (isCollapsed(editor.selection)) { + const entry = findNode(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; diff --git a/apps/www/src/types/plate-types.ts b/apps/www/src/types/plate-types.ts index 1750a300f5..ae0fca7578 100644 --- a/apps/www/src/types/plate-types.ts +++ b/apps/www/src/types/plate-types.ts @@ -45,9 +45,6 @@ import { TReactEditor, useEditorRef, useEditorState, - usePlateActions, - usePlateSelectors, - usePlateStates, WithOverride, } from '@udecode/plate-common'; import { @@ -371,12 +368,6 @@ export const getMyEditor = (editor: MyEditor) => getTEditor(editor); export const useMyEditorRef = () => useEditorRef(); export const useMyEditorState = () => useEditorState(); -export const useMyPlateSelectors = (id?: PlateId) => - usePlateSelectors(id); -export const useMyPlateActions = (id?: PlateId) => - usePlateActions(id); -export const useMyPlateStates = (id?: PlateId) => - usePlateStates(id); /** * Utils diff --git a/packages/alignment/src/hooks/useAlignDropdownMenu.ts b/packages/alignment/src/hooks/useAlignDropdownMenu.ts index a53fda7208..5fdaff1d33 100644 --- a/packages/alignment/src/hooks/useAlignDropdownMenu.ts +++ b/packages/alignment/src/hooks/useAlignDropdownMenu.ts @@ -4,26 +4,28 @@ import { isCollapsed, isDefined, useEditorRef, - useEditorState, + useEditorSelector, } from '@udecode/plate-common'; import { Alignment, KEY_ALIGN, setAlign } from '../index'; export const useAlignDropdownMenuState = () => { - const editor = useEditorState(); - - let value: Alignment = 'left'; - if (isCollapsed(editor?.selection)) { - const entry = findNode(editor!, { - match: (n) => isDefined(n[KEY_ALIGN]), - }); - if (entry) { - const nodeValue = entry[0][KEY_ALIGN] as string; - if (nodeValue === 'right') value = 'right'; - if (nodeValue === 'center') value = 'center'; - if (nodeValue === 'justify') value = 'justify'; + const value: Alignment = useEditorSelector((editor) => { + if (isCollapsed(editor.selection)) { + const entry = findNode(editor, { + match: (n) => isDefined(n[KEY_ALIGN]), + }); + + if (entry) { + const nodeValue = entry[0][KEY_ALIGN] as string; + if (nodeValue === 'right') return 'right'; + if (nodeValue === 'center') return 'center'; + if (nodeValue === 'justify') return 'justify'; + } } - } + + return 'left'; + }, []); return { value, diff --git a/packages/comments/src/components/CommentDeleteButton.tsx b/packages/comments/src/components/CommentDeleteButton.tsx index 66ae8e0530..f36a8fec0d 100644 --- a/packages/comments/src/components/CommentDeleteButton.tsx +++ b/packages/comments/src/components/CommentDeleteButton.tsx @@ -10,7 +10,7 @@ import { unsetCommentNodesById } from '../utils/index'; export const useCommentDeleteButtonState = () => { const activeCommentId = useCommentsSelectors().activeCommentId(); - const onCommentDelete = useCommentsSelectors().onCommentDelete()?.fn; + const onCommentDelete = useCommentsSelectors().onCommentDelete(); const id = useCommentSelectors().id(); const setActiveCommentId = useCommentsActions().activeCommentId(); const removeComment = useRemoveComment(); diff --git a/packages/comments/src/components/CommentEditSaveButton.tsx b/packages/comments/src/components/CommentEditSaveButton.tsx index 1307e5c2b7..558bea509e 100644 --- a/packages/comments/src/components/CommentEditSaveButton.tsx +++ b/packages/comments/src/components/CommentEditSaveButton.tsx @@ -12,7 +12,7 @@ import { } from '../stores/comments/CommentsProvider'; export const useCommentEditSaveButtonState = () => { - const onCommentUpdate = useCommentsSelectors().onCommentUpdate()?.fn; + const onCommentUpdate = useCommentsSelectors().onCommentUpdate(); const editingValue = useCommentSelectors().editingValue(); const setEditingValue = useCommentActions().editingValue(); const id = useCommentSelectors().id(); diff --git a/packages/comments/src/components/CommentNewSubmitButton.tsx b/packages/comments/src/components/CommentNewSubmitButton.tsx index 4a8338538d..3ed7384626 100644 --- a/packages/comments/src/components/CommentNewSubmitButton.tsx +++ b/packages/comments/src/components/CommentNewSubmitButton.tsx @@ -12,7 +12,7 @@ import { } from '../stores/comments/CommentsProvider'; export const useCommentNewSubmitButtonState = () => { - const onCommentAdd = useCommentsSelectors().onCommentAdd()?.fn; + const onCommentAdd = useCommentsSelectors().onCommentAdd(); const activeCommentId = useCommentsSelectors().activeCommentId()!; const comment = useComment(SCOPE_ACTIVE_COMMENT)!; const newValue = useCommentsSelectors().newValue(); diff --git a/packages/comments/src/components/CommentResolveButton.tsx b/packages/comments/src/components/CommentResolveButton.tsx index 8f7f83421d..63eb855669 100644 --- a/packages/comments/src/components/CommentResolveButton.tsx +++ b/packages/comments/src/components/CommentResolveButton.tsx @@ -8,7 +8,7 @@ import { } from '../stores/comments/CommentsProvider'; export const useCommentResolveButton = () => { - const onCommentUpdate = useCommentsSelectors().onCommentUpdate()?.fn; + const onCommentUpdate = useCommentsSelectors().onCommentUpdate(); const activeCommentId = useCommentsSelectors().activeCommentId(); const setActiveCommentId = useCommentsActions().activeCommentId(); const updateComment = useUpdateComment(activeCommentId); diff --git a/packages/comments/src/stores/comments/CommentsProvider.tsx b/packages/comments/src/stores/comments/CommentsProvider.tsx index 5061f781ac..1e439c4575 100644 --- a/packages/comments/src/stores/comments/CommentsProvider.tsx +++ b/packages/comments/src/stores/comments/CommentsProvider.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { createAtomStore, getNodeString, @@ -36,57 +35,31 @@ export interface CommentsStoreState { focusTextarea: boolean; - onCommentAdd: { fn: (value: WithPartial) => void } | null; - onCommentUpdate: { - fn: (value: Pick & Partial>) => void; - } | null; - onCommentDelete: { fn: (id: string) => void } | null; + onCommentAdd: ((value: WithPartial) => void) | null; + onCommentUpdate: + | ((value: Pick & Partial>) => void) + | null; + onCommentDelete: ((id: string) => void) | null; } -export const { - commentsStore, - useCommentsStore, - CommentsProvider: PrimitiveCommentsProvider, -} = createAtomStore( - { - myUserId: null, - users: {}, - comments: {}, - activeCommentId: null, - addingCommentId: null, - newValue: [{ type: 'p', children: [{ text: '' }] }], - focusTextarea: false, - onCommentAdd: null, - onCommentUpdate: null, - onCommentDelete: null, - } as CommentsStoreState, - { - name: 'comments', - } -); - -export const CommentsProvider = ({ - onCommentAdd, - onCommentUpdate, - onCommentDelete, - ...props -}: Omit< - React.ComponentProps, - 'onCommentAdd' | 'onCommentUpdate' | 'onCommentDelete' -> & { - onCommentAdd?: (value: WithPartial) => void; - onCommentUpdate?: ( - value: Pick & Partial> - ) => void; - onCommentDelete?: (id: string) => void; -}) => ( - -); +export const { commentsStore, useCommentsStore, CommentsProvider } = + createAtomStore( + { + myUserId: null, + users: {}, + comments: {}, + activeCommentId: null, + addingCommentId: null, + newValue: [{ type: 'p', children: [{ text: '' }] }], + focusTextarea: false, + onCommentAdd: null, + onCommentUpdate: null, + onCommentDelete: null, + } as CommentsStoreState, + { + name: 'comments', + } + ); export const useCommentsStates = () => useCommentsStore().use; export const useCommentsSelectors = () => useCommentsStore().get; diff --git a/packages/core/package.json b/packages/core/package.json index f46807dbaa..977e26fb57 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,7 +46,7 @@ "clsx": "^1.2.1", "is-hotkey": "^0.2.0", "jotai": "^2.6.0", - "jotai-x": "^1.0.1", + "jotai-x": "^1.1.0", "lodash": "^4.17.21", "nanoid": "^3.3.6", "react-hotkeys-hook": "^4.4.1", diff --git a/packages/core/src/components/EditorRefEffect.tsx b/packages/core/src/components/EditorRefEffect.tsx index 672f89f2c2..5164af9996 100644 --- a/packages/core/src/components/EditorRefEffect.tsx +++ b/packages/core/src/components/EditorRefEffect.tsx @@ -26,7 +26,7 @@ export function EditorRefEffect({ id }: { id?: PlateId }) { const setIsMounted = usePlateActions(id).isMounted(); const plugins = usePlateSelectors(id).plugins(); const editorState = useEditorRef(id); - const editorRef = usePlateSelectors(id).editorRef()?.ref; + const editorRef = usePlateSelectors(id).editorRef(); useEffect(() => { setIsMounted(true); diff --git a/packages/core/src/components/Plate.tsx b/packages/core/src/components/Plate.tsx index a73c7792d5..f963a537ac 100644 --- a/packages/core/src/components/Plate.tsx +++ b/packages/core/src/components/Plate.tsx @@ -142,11 +142,11 @@ function PlateInner< rawPlugins={pluginsProp} readOnly={readOnly} value={value} - decorate={{ fn: decorate as any }} - onChange={{ fn: onChange as any }} - editorRef={{ ref: editorRef as any }} - renderElement={{ fn: renderElement as any }} - renderLeaf={{ fn: renderLeaf as any }} + decorate={decorate} + onChange={onChange as PlateStoreState['onChange']} + editorRef={editorRef as PlateStoreState['editorRef']} + renderElement={renderElement} + renderLeaf={renderLeaf} scope={id} > { return pipeDecorate(editor, storeDecorate ?? editableProps?.decorate); diff --git a/packages/core/src/hooks/usePlateEffects.ts b/packages/core/src/hooks/usePlateEffects.ts index a9a405ab62..36506dd36e 100644 --- a/packages/core/src/hooks/usePlateEffects.ts +++ b/packages/core/src/hooks/usePlateEffects.ts @@ -21,7 +21,7 @@ export const usePlateEffects = < }: UsePlateEffectsProps) => { const editor = useEditorRef(id); - const states = usePlateStates(id); + const states = usePlateStates(id); const [rawPlugins, setRawPlugins] = states.rawPlugins(); const [, setPlugins] = states.plugins(); diff --git a/packages/core/src/hooks/useSlateProps.ts b/packages/core/src/hooks/useSlateProps.ts index 3b2d3822d3..aa1a41998a 100644 --- a/packages/core/src/hooks/useSlateProps.ts +++ b/packages/core/src/hooks/useSlateProps.ts @@ -17,7 +17,7 @@ export const useSlateProps = ({ const editor = useEditorRef(id); const value = usePlateSelectors(id).value(); const setValue = usePlateActions(id).value(); - const onChangeProp = usePlateSelectors(id).onChange()?.fn; + const onChangeProp = usePlateSelectors(id).onChange(); const onChange = useCallback( (newValue: V) => { diff --git a/packages/core/src/libs/jotai.ts b/packages/core/src/libs/jotai.ts index 8be65074ba..7a7242d445 100644 --- a/packages/core/src/libs/jotai.ts +++ b/packages/core/src/libs/jotai.ts @@ -1,2 +1 @@ -export type { GetRecord, SetRecord, UseRecord } from 'jotai-x'; export { createAtomStore } from 'jotai-x'; diff --git a/packages/core/src/stores/plate/createPlateStore.ts b/packages/core/src/stores/plate/createPlateStore.ts index 9ac7ecd6b3..501acc4357 100644 --- a/packages/core/src/stores/plate/createPlateStore.ts +++ b/packages/core/src/stores/plate/createPlateStore.ts @@ -1,11 +1,7 @@ import { Value } from '@udecode/slate'; +import { atom } from 'jotai'; -import { - createAtomStore, - GetRecord, - SetRecord, - UseRecord, -} from '../../libs/jotai'; +import { createAtomStore } from '../../libs/jotai'; import { PlateEditor } from '../../types/PlateEditor'; import { PlateStoreState } from '../../types/PlateStore'; @@ -61,6 +57,16 @@ export const createPlateStore = < } as PlateStoreState, { name: 'plate', + extend: (atoms) => ({ + trackedEditor: atom((get) => ({ + editor: get(atoms.editor), + version: get(atoms.versionEditor), + })), + trackedSelection: atom((get) => ({ + selection: get(atoms.editor).selection, + version: get(atoms.versionSelection), + })), + }), } ); @@ -70,24 +76,9 @@ export const { PlateProvider: PlateStoreProvider, } = createPlateStore(); -export const usePlateSelectors = < - V extends Value = Value, - E extends PlateEditor = PlateEditor, ->( - id?: PlateId -): GetRecord> => usePlateStore(id).get as any; -export const usePlateActions = < - V extends Value = Value, - E extends PlateEditor = PlateEditor, ->( - id?: PlateId -): SetRecord> => usePlateStore(id).set as any; -export const usePlateStates = < - V extends Value = Value, - E extends PlateEditor = PlateEditor, ->( - id?: PlateId -): UseRecord> => usePlateStore(id).use as any; +export const usePlateSelectors = (id?: PlateId) => usePlateStore(id).get; +export const usePlateActions = (id?: PlateId) => usePlateStore(id).set; +export const usePlateStates = (id?: PlateId) => usePlateStore(id).use; /** * Get the closest `Plate` id. diff --git a/packages/core/src/stores/plate/selectors/index.ts b/packages/core/src/stores/plate/selectors/index.ts index fdf70483ff..dd84c5c107 100644 --- a/packages/core/src/stores/plate/selectors/index.ts +++ b/packages/core/src/stores/plate/selectors/index.ts @@ -5,6 +5,7 @@ export * from './useEditorReadOnly'; export * from './useEditorRef'; export * from './useEditorSelection'; +export * from './useEditorSelector'; export * from './useEditorState'; export * from './useEditorVersion'; export * from './useSelectionVersion'; diff --git a/packages/core/src/stores/plate/selectors/useEditorRef.ts b/packages/core/src/stores/plate/selectors/useEditorRef.ts index f2c4e7b9db..b4ce26a16a 100644 --- a/packages/core/src/stores/plate/selectors/useEditorRef.ts +++ b/packages/core/src/stores/plate/selectors/useEditorRef.ts @@ -11,4 +11,4 @@ export const useEditorRef = < E extends PlateEditor = PlateEditor, >( id?: PlateId -) => usePlateSelectors(id).editor(); +): E => usePlateSelectors(id).editor() as any; diff --git a/packages/core/src/stores/plate/selectors/useEditorSelection.ts b/packages/core/src/stores/plate/selectors/useEditorSelection.ts index 1e7bc883a1..8e4b3143de 100644 --- a/packages/core/src/stores/plate/selectors/useEditorSelection.ts +++ b/packages/core/src/stores/plate/selectors/useEditorSelection.ts @@ -1,12 +1,7 @@ -import { PlateId } from '../createPlateStore'; -import { useEditorRef } from './useEditorRef'; -import { useSelectionVersion } from './useSelectionVersion'; +import { PlateId, usePlateSelectors } from '../createPlateStore'; /** * Get the editor selection (deeply memoized). */ -export const useEditorSelection = (id?: PlateId) => { - useSelectionVersion(id); - - return useEditorRef(id).selection; -}; +export const useEditorSelection = (id?: PlateId) => + usePlateSelectors(id).trackedSelection().selection; diff --git a/packages/core/src/stores/plate/selectors/useEditorSelector.ts b/packages/core/src/stores/plate/selectors/useEditorSelector.ts new file mode 100644 index 0000000000..b24a06a18a --- /dev/null +++ b/packages/core/src/stores/plate/selectors/useEditorSelector.ts @@ -0,0 +1,34 @@ +import { DependencyList, useMemo } from 'react'; +import { Value } from '@udecode/slate'; +import { selectAtom } from 'jotai/utils'; + +import { PlateEditor } from '../../../types/PlateEditor'; +import { PlateId, plateStore, usePlateSelectors } from '../createPlateStore'; + +export interface UseEditorSelectorOptions { + id?: PlateId; + equalityFn?: (a: T, b: T) => boolean; +} + +export const useEditorSelector = < + T, + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + selector: (editor: E, prev?: T) => T, + deps: DependencyList, + { id, equalityFn = (a: T, b: T) => a === b }: UseEditorSelectorOptions = {} +): T => { + const selectorAtom = useMemo( + () => + selectAtom<{ editor: E }, T>( + plateStore.atom.trackedEditor, + ({ editor }, prev) => selector(editor, prev), + equalityFn + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + deps + ); + + return usePlateSelectors(id).atom(selectorAtom); +}; diff --git a/packages/core/src/stores/plate/selectors/useEditorState.ts b/packages/core/src/stores/plate/selectors/useEditorState.ts index 06171d050d..e8793afc74 100644 --- a/packages/core/src/stores/plate/selectors/useEditorState.ts +++ b/packages/core/src/stores/plate/selectors/useEditorState.ts @@ -1,9 +1,7 @@ import { Value } from '@udecode/slate'; import { PlateEditor } from '../../../types/PlateEditor'; -import { PlateId } from '../createPlateStore'; -import { useEditorRef } from './useEditorRef'; -import { useEditorVersion } from './useEditorVersion'; +import { PlateId, usePlateSelectors } from '../createPlateStore'; /** * Get editor state which is updated on editor change. @@ -13,8 +11,6 @@ export const useEditorState = < E extends PlateEditor = PlateEditor, >( id?: PlateId -) => { - useEditorVersion(id); - - return useEditorRef(id); +): E => { + return usePlateSelectors(id).trackedEditor().editor; }; diff --git a/packages/core/src/types/PlateEditorMethods.ts b/packages/core/src/types/PlateEditorMethods.ts index ec89ab748b..d3fdb374f4 100644 --- a/packages/core/src/types/PlateEditorMethods.ts +++ b/packages/core/src/types/PlateEditorMethods.ts @@ -2,8 +2,6 @@ import { Value } from '@udecode/slate'; import { EXPOSED_STORE_KEYS, PlateStoreState } from './PlateStore'; -import type { SetRecord } from '../libs/jotai'; - export type PlateEditorMethods = { reset: () => void; redecorate: () => void; @@ -11,9 +9,9 @@ export type PlateEditorMethods = { // Example: editor.plate.set.readOnly(true) plate: { set: { - [K in (typeof EXPOSED_STORE_KEYS)[number]]: ReturnType< - SetRecord>[K] - >; + [K in (typeof EXPOSED_STORE_KEYS)[number]]: ( + value: PlateStoreState[K] + ) => void; }; }; }; diff --git a/packages/core/src/types/PlateStore.ts b/packages/core/src/types/PlateStore.ts index 0875edea3a..d29492f974 100644 --- a/packages/core/src/types/PlateStore.ts +++ b/packages/core/src/types/PlateStore.ts @@ -76,16 +76,16 @@ export type PlateStoreState< /** * Controlled callback called when the editor state changes. */ - onChange: { fn: (value: V) => void }; + onChange: (value: V) => void; /** * Access the editor object using a React ref. */ - editorRef: { ref: ForwardedRef }; + editorRef: ForwardedRef; - decorate: { fn: NonNullable }; - renderElement: { fn: NonNullable }; - renderLeaf: { fn: NonNullable }; + decorate: NonNullable; + renderElement: NonNullable; + renderLeaf: NonNullable; }>; // A list of store keys to be exposed in `editor.plate.set`. diff --git a/packages/emoji/src/hooks/useEmojiDropdownMenuState.ts b/packages/emoji/src/hooks/useEmojiDropdownMenuState.ts index 9efe3e5769..99dfed7248 100644 --- a/packages/emoji/src/hooks/useEmojiDropdownMenuState.ts +++ b/packages/emoji/src/hooks/useEmojiDropdownMenuState.ts @@ -1,4 +1,4 @@ -import { useEditorState, useStableMemo } from '@udecode/plate-common'; +import { useStableMemo } from '@udecode/plate-common'; import { EmojiFloatingIndexSearch, @@ -18,8 +18,6 @@ export function useEmojiDropdownMenuState({ settings = EmojiSettings, closeOnSelect = true, }: EmojiDropdownMenuOptions = {}) { - const editor = useEditorState(); - const [emojiLibrary, indexSearch] = useStableMemo(() => { const frequentEmojiStorage = new FrequentEmojiStorage({ limit: settings.showFrequent.limit, @@ -39,7 +37,6 @@ export function useEmojiDropdownMenuState({ const { isOpen, setIsOpen, ...emojiPickerState } = useEmojiPicker({ closeOnSelect, - editor, emojiLibrary, indexSearch, }); diff --git a/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts b/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts index ed9a538eba..20ca75abcc 100644 --- a/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts +++ b/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts @@ -6,7 +6,7 @@ import { useEffect, useRef, } from 'react'; -import { PlateEditor } from '@udecode/plate-common'; +import { useEditorRef } from '@udecode/plate-common'; import { i18n } from '../../constants'; import { getEmojiOnInsert } from '../../handlers/getEmojiOnInsert'; @@ -31,7 +31,6 @@ export type MutableRefs = MutableRefObject<{ export type UseEmojiPickerProps = { closeOnSelect: boolean; - editor: PlateEditor; emojiLibrary: IEmojiFloatingLibrary; indexSearch: AIndexSearch; }; @@ -62,11 +61,12 @@ export type UseEmojiPickerType< }; export const useEmojiPicker = ({ - editor, emojiLibrary, indexSearch, closeOnSelect, }: UseEmojiPickerProps): Omit => { + const editor = useEditorRef(); + const [state, dispatch] = EmojiPickerState(); const refs = useRef({ contentRoot: createRef(), diff --git a/packages/floating/src/hooks/useFloatingToolbar.ts b/packages/floating/src/hooks/useFloatingToolbar.ts index 67fafe66b3..ebb464d311 100644 --- a/packages/floating/src/hooks/useFloatingToolbar.ts +++ b/packages/floating/src/hooks/useFloatingToolbar.ts @@ -3,8 +3,9 @@ import { getSelectionText, isSelectionExpanded, mergeProps, - useEditorState, + useEditorSelector, useEventEditorSelectors, + usePlateSelectors, } from '@udecode/plate-common'; import { useFocused } from 'slate-react'; @@ -25,7 +26,10 @@ export const useFloatingToolbarState = ({ hideToolbar, ignoreReadOnly, }: FloatingToolbarState) => { - const editor = useEditorState(); + const editorId = usePlateSelectors().id(); + const selectionExpanded = useEditorSelector(isSelectionExpanded, []); + const selectionText = useEditorSelector(getSelectionText, []); + const focusedEditorId = useEventEditorSelectors.focus(); const focused = useFocused(); @@ -33,9 +37,6 @@ export const useFloatingToolbarState = ({ const [waitForCollapsedSelection, setWaitForCollapsedSelection] = useState(false); - const selectionExpanded = editor && isSelectionExpanded(editor); - const selectionText = editor && getSelectionText(editor); - const floating = useVirtualFloating( mergeProps( { @@ -48,7 +49,7 @@ export const useFloatingToolbarState = ({ ); return { - editor, + editorId, open, setOpen, waitForCollapsedSelection, @@ -64,7 +65,7 @@ export const useFloatingToolbarState = ({ }; export const useFloatingToolbar = ({ - editor, + editorId, selectionExpanded, selectionText, waitForCollapsedSelection, @@ -98,7 +99,7 @@ export const useFloatingToolbar = ({ if ( !selectionExpanded || !selectionText || - (!(editor.id === focusedEditorId || ignoreReadOnly) && hideToolbar) + (!(editorId === focusedEditorId || ignoreReadOnly) && hideToolbar) ) { setOpen(false); } else if ( @@ -110,8 +111,7 @@ export const useFloatingToolbar = ({ } }, [ setOpen, - editor.id, - editor.selection, + editorId, focusedEditorId, hideToolbar, ignoreReadOnly, diff --git a/packages/font/src/hooks/useColorDropdownMenu.ts b/packages/font/src/hooks/useColorDropdownMenu.ts index 4b33984b9d..30cf032468 100644 --- a/packages/font/src/hooks/useColorDropdownMenu.ts +++ b/packages/font/src/hooks/useColorDropdownMenu.ts @@ -5,7 +5,8 @@ import { removeMark, select, setMarks, - useEditorState, + useEditorRef, + useEditorSelector, } from '@udecode/plate-common'; export const useColorDropdownMenuState = ({ @@ -19,9 +20,19 @@ export const useColorDropdownMenuState = ({ customColors: { name: string; value: string; isBrightColor: boolean }[]; closeOnSelect?: boolean; }) => { - const editor = useEditorState(); + const editor = useEditorRef(); - const color = editor && (getMark(editor, nodeType) as string); + const selectionDefined = useEditorSelector( + // eslint-disable-next-line @typescript-eslint/no-shadow + (editor) => !!editor.selection, + [] + ); + + const color = useEditorSelector( + // eslint-disable-next-line @typescript-eslint/no-shadow + (editor) => getMark(editor, nodeType) as string, + [nodeType] + ); const [selectedColor, setSelectedColor] = useState(); @@ -35,7 +46,7 @@ export const useColorDropdownMenuState = ({ const updateColor = useCallback( (value: string) => { - if (editor && editor && editor.selection) { + if (editor.selection) { setSelectedColor(value); select(editor, editor.selection); @@ -56,7 +67,7 @@ export const useColorDropdownMenuState = ({ ); const clearColor = useCallback(() => { - if (editor && editor && editor.selection) { + if (editor.selection) { select(editor, editor.selection); focusEditor(editor); @@ -69,10 +80,10 @@ export const useColorDropdownMenuState = ({ }, [editor, selectedColor, closeOnSelect, onToggle, nodeType]); useEffect(() => { - if (editor?.selection) { + if (selectionDefined) { setSelectedColor(color); } - }, [color, editor?.selection]); + }, [color, selectionDefined]); return { open, diff --git a/packages/indent-list/src/hooks/useIndentListToolbarButton.ts b/packages/indent-list/src/hooks/useIndentListToolbarButton.ts index fcc32f2431..80007c76bb 100644 --- a/packages/indent-list/src/hooks/useIndentListToolbarButton.ts +++ b/packages/indent-list/src/hooks/useIndentListToolbarButton.ts @@ -1,4 +1,4 @@ -import { useEditorRef, useEditorState } from '@udecode/plate-common'; +import { useEditorRef, useEditorSelector } from '@udecode/plate-common'; import { ListStyleType, toggleIndentList } from '../index'; import { someIndentList } from './someIndentList'; @@ -6,10 +6,13 @@ import { someIndentList } from './someIndentList'; export const useIndentListToolbarButtonState = ({ nodeType = ListStyleType.Disc, }: { nodeType?: string } = {}) => { - const editor = useEditorState(); + const pressed = useEditorSelector( + (editor) => someIndentList(editor, nodeType), + [nodeType] + ); return { - pressed: someIndentList(editor, nodeType), + pressed, nodeType, }; }; diff --git a/packages/line-height/src/hooks/useLineHeightDropdownMenu.ts b/packages/line-height/src/hooks/useLineHeightDropdownMenu.ts index 14151e3679..27404fa273 100644 --- a/packages/line-height/src/hooks/useLineHeightDropdownMenu.ts +++ b/packages/line-height/src/hooks/useLineHeightDropdownMenu.ts @@ -5,25 +5,28 @@ import { isCollapsed, TElement, useEditorRef, - useEditorState, + useEditorSelector, } from '@udecode/plate-common'; import { KEY_LINE_HEIGHT, setLineHeight } from '../index'; export const useLineHeightDropdownMenuState = () => { - const editor = useEditorState(); + const editor = useEditorRef(); const { validNodeValues: values = [], defaultNodeValue } = getPluginInjectProps(editor, KEY_LINE_HEIGHT); - let value: string | undefined; - if (isCollapsed(editor?.selection)) { - const entry = getBlockAbove(editor); - if (entry) { - value = - values.find((item) => item === entry[0][KEY_LINE_HEIGHT]) ?? - defaultNodeValue; + // eslint-disable-next-line @typescript-eslint/no-shadow + const value: string | undefined = useEditorSelector((editor) => { + if (isCollapsed(editor.selection)) { + const entry = getBlockAbove(editor); + if (entry) { + return ( + values.find((item) => item === entry[0][KEY_LINE_HEIGHT]) ?? + defaultNodeValue + ); + } } - } + }, []); return { value, diff --git a/packages/link/src/components/useLinkToolbarButton.ts b/packages/link/src/components/useLinkToolbarButton.ts index c6529a502b..b2fdeff198 100644 --- a/packages/link/src/components/useLinkToolbarButton.ts +++ b/packages/link/src/components/useLinkToolbarButton.ts @@ -2,16 +2,20 @@ import { getPluginType, someNode, useEditorRef, - useEditorState, + useEditorSelector, } from '@udecode/plate-common'; import { ELEMENT_LINK, triggerFloatingLink } from '../index'; export const useLinkToolbarButtonState = () => { - const editor = useEditorState(); - const pressed = - !!editor?.selection && - someNode(editor, { match: { type: getPluginType(editor, ELEMENT_LINK) } }); + const pressed = useEditorSelector( + (editor) => + !!editor?.selection && + someNode(editor, { + match: { type: getPluginType(editor, ELEMENT_LINK) }, + }), + [] + ); return { pressed, diff --git a/packages/list/src/hooks/useListToolbarButton.ts b/packages/list/src/hooks/useListToolbarButton.ts index 9e64afb489..0de05dc2b2 100644 --- a/packages/list/src/hooks/useListToolbarButton.ts +++ b/packages/list/src/hooks/useListToolbarButton.ts @@ -2,16 +2,18 @@ import { getPluginType, someNode, useEditorRef, - useEditorState, + useEditorSelector, } from '@udecode/plate-common'; import { ELEMENT_UL, toggleList } from '../index'; export const useListToolbarButtonState = ({ nodeType = ELEMENT_UL } = {}) => { - const editor = useEditorState(); - const pressed = - !!editor?.selection && - someNode(editor, { match: { type: getPluginType(editor, nodeType) } }); + const pressed = useEditorSelector( + (editor) => + !!editor.selection && + someNode(editor, { match: { type: getPluginType(editor, nodeType) } }), + [nodeType] + ); return { pressed, diff --git a/packages/plate-utils/src/hooks/useMarkToolbarButton.ts b/packages/plate-utils/src/hooks/useMarkToolbarButton.ts index 00f3e689de..fd4a14d752 100644 --- a/packages/plate-utils/src/hooks/useMarkToolbarButton.ts +++ b/packages/plate-utils/src/hooks/useMarkToolbarButton.ts @@ -1,4 +1,4 @@ -import { useEditorRef, useEditorState } from '@udecode/plate-core'; +import { useEditorRef, useEditorSelector } from '@udecode/plate-core'; import { isMarkActive, toggleMark } from '@udecode/slate-utils'; export const useMarkToolbarButtonState = ({ @@ -8,8 +8,10 @@ export const useMarkToolbarButtonState = ({ nodeType: string; clear?: string | string[]; }) => { - const editor = useEditorState(); - const pressed = !!editor?.selection && isMarkActive(editor, nodeType); + const pressed = useEditorSelector( + (editor) => isMarkActive(editor, nodeType), + [nodeType] + ); return { pressed, diff --git a/packages/resizable/src/components/Resizable.tsx b/packages/resizable/src/components/Resizable.tsx index 9dfcb73a00..ca2c48baf6 100644 --- a/packages/resizable/src/components/Resizable.tsx +++ b/packages/resizable/src/components/Resizable.tsx @@ -137,7 +137,7 @@ const Resizable = React.forwardRef< return (
- + {children}
diff --git a/packages/resizable/src/components/ResizeHandle.tsx b/packages/resizable/src/components/ResizeHandle.tsx index c55e95edc3..a2ea985db9 100644 --- a/packages/resizable/src/components/ResizeHandle.tsx +++ b/packages/resizable/src/components/ResizeHandle.tsx @@ -14,9 +14,7 @@ import { ResizeDirection, ResizeEvent } from '../types'; import { isTouchEvent } from '../utils'; export type ResizeHandleStoreState = { - onResize: { - fn: (event: ResizeEvent) => void; - }; + onResize: (event: ResizeEvent) => void; }; const initialState: Nullable = { @@ -48,7 +46,7 @@ export const useResizeHandleState = ({ onHoverEnd, }: ResizeHandleOptions) => { const onResizeStore = useResizeHandleStore().get.onResize(); - const onResize = onResizeProp ?? onResizeStore.fn; + const onResize = onResizeProp ?? onResizeStore; const [isResizing, setIsResizing] = useState(false); const [initialPosition, setInitialPosition] = useState(0); diff --git a/packages/tabbable/src/TabbableEffects.tsx b/packages/tabbable/src/TabbableEffects.tsx index 262910ddc6..67b3346e67 100644 --- a/packages/tabbable/src/TabbableEffects.tsx +++ b/packages/tabbable/src/TabbableEffects.tsx @@ -6,7 +6,7 @@ import { toDOMNode, toSlateNode, useEditorReadOnly, - useEditorState, + useEditorRef, } from '@udecode/plate-common'; import { Path } from 'slate'; import { tabbable } from 'tabbable'; @@ -16,14 +16,15 @@ import { findTabDestination } from './findTabDestination'; import { TabbableEntry, TabbablePlugin } from './types'; export function TabbableEffects() { - const editor = useEditorState(); + const editor = useEditorRef(); const readOnly = useEditorReadOnly(); - const { query, globalEventListener, insertTabbableEntries, isTabbable } = - getPluginOptions(editor, KEY_TABBABLE); useEffect(() => { if (readOnly) return; + const { query, globalEventListener, insertTabbableEntries, isTabbable } = + getPluginOptions(editor, KEY_TABBABLE); + const editorDOMNode = toDOMNode(editor, editor); if (!editorDOMNode) return; @@ -127,14 +128,7 @@ export function TabbableEffects() { eventListenerNode.addEventListener('keydown', handler, true); return () => eventListenerNode.removeEventListener('keydown', handler, true); - }, [ - readOnly, - editor, - globalEventListener, - isTabbable, - insertTabbableEntries, - query, - ]); + }, [readOnly, editor]); return null; } diff --git a/packages/table/src/components/TableCellElement/useTableBordersDropdownMenuContentState.ts b/packages/table/src/components/TableCellElement/useTableBordersDropdownMenuContentState.ts index 00da8b1105..4fd745d92f 100644 --- a/packages/table/src/components/TableCellElement/useTableBordersDropdownMenuContentState.ts +++ b/packages/table/src/components/TableCellElement/useTableBordersDropdownMenuContentState.ts @@ -1,17 +1,31 @@ -import { useEditorState } from '@udecode/plate-common'; +import { useEditorRef, useEditorSelector } from '@udecode/plate-common'; import { isTableBorderHidden } from '../../queries/index'; import { useTableStore } from '../../stores/index'; import { getOnSelectTableBorderFactory } from './getOnSelectTableBorderFactory'; export const useTableBordersDropdownMenuContentState = () => { - const editor = useEditorState(); + const editor = useEditorRef(); const selectedCells = useTableStore().get.selectedCells(); - const hasBottomBorder = !isTableBorderHidden(editor, 'bottom'); - const hasTopBorder = !isTableBorderHidden(editor, 'top'); - const hasLeftBorder = !isTableBorderHidden(editor, 'left'); - const hasRightBorder = !isTableBorderHidden(editor, 'right'); + /* eslint-disable @typescript-eslint/no-shadow */ + const hasBottomBorder = useEditorSelector( + (editor) => !isTableBorderHidden(editor, 'bottom'), + [] + ); + const hasTopBorder = useEditorSelector( + (editor) => !isTableBorderHidden(editor, 'top'), + [] + ); + const hasLeftBorder = useEditorSelector( + (editor) => !isTableBorderHidden(editor, 'left'), + [] + ); + const hasRightBorder = useEditorSelector( + (editor) => !isTableBorderHidden(editor, 'right'), + [] + ); + /* eslint-enable @typescript-eslint/no-shadow */ const hasOuterBorders = hasBottomBorder && hasTopBorder && hasLeftBorder && hasRightBorder; diff --git a/packages/table/src/merge/useTableMergeState.ts b/packages/table/src/merge/useTableMergeState.ts index f142dd8f37..0ff2f09776 100644 --- a/packages/table/src/merge/useTableMergeState.ts +++ b/packages/table/src/merge/useTableMergeState.ts @@ -1,11 +1,9 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import { useMemo } from 'react'; import { getPluginOptions, - isCollapsed, - isExpanded, + isSelectionExpanded, useEditorRef, - useEditorState, + useEditorSelector, } from '@udecode/plate-common'; import { useReadOnly, useSelected } from 'slate-react'; @@ -27,32 +25,27 @@ export const useTableMergeState = () => { if (!enableMerging) return { canMerge: false, canUnmerge: false }; - const editor = useEditorState(); - const readOnly = useReadOnly(); const selected = useSelected(); + const selectionExpanded = useEditorSelector(isSelectionExpanded, []); - const collapsed = !readOnly && selected && isCollapsed(editor.selection); + const collapsed = !readOnly && selected && !selectionExpanded; const selectedTables = useTableStore().get.selectedTable(); const selectedTable = selectedTables?.[0]; - const selectedCellEntries = useMemo( - () => + const selectedCellEntries = useEditorSelector( + (editor) => getTableGridAbove(editor, { format: 'cell', }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [editor.selection] + [] ); - const canMerge = useMemo(() => { - return ( - !readOnly && - selected && - isExpanded(editor.selection) && - isTableRectangular(selectedTable) - ); - }, [readOnly, selected, editor.selection, selectedTable]); + const canMerge = + !readOnly && + selected && + selectionExpanded && + isTableRectangular(selectedTable); const canUnmerge = collapsed && diff --git a/yarn.lock b/yarn.lock index 4ef253b3f6..ed797cb0d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6194,7 +6194,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-comments@npm:27.0.3, @udecode/plate-comments@workspace:^, @udecode/plate-comments@workspace:packages/comments": +"@udecode/plate-comments@npm:27.0.4, @udecode/plate-comments@workspace:^, @udecode/plate-comments@workspace:packages/comments": version: 0.0.0-use.local resolution: "@udecode/plate-comments@workspace:packages/comments" dependencies: @@ -6241,7 +6241,7 @@ __metadata: clsx: "npm:^1.2.1" is-hotkey: "npm:^0.2.0" jotai: "npm:^2.6.0" - jotai-x: "npm:^1.0.1" + jotai-x: "npm:^1.1.0" lodash: "npm:^4.17.21" nanoid: "npm:^3.3.6" react-hotkeys-hook: "npm:^4.4.1" @@ -6907,7 +6907,7 @@ __metadata: "@udecode/plate-break": "npm:27.0.3" "@udecode/plate-code-block": "npm:27.0.3" "@udecode/plate-combobox": "npm:27.0.3" - "@udecode/plate-comments": "npm:27.0.3" + "@udecode/plate-comments": "npm:27.0.4" "@udecode/plate-common": "npm:27.0.3" "@udecode/plate-find-replace": "npm:27.0.3" "@udecode/plate-floating": "npm:27.0.3" @@ -13522,9 +13522,9 @@ __metadata: languageName: node linkType: hard -"jotai-x@npm:^1.0.1": - version: 1.0.1 - resolution: "jotai-x@npm:1.0.1" +"jotai-x@npm:^1.1.0": + version: 1.1.0 + resolution: "jotai-x@npm:1.1.0" peerDependencies: "@types/react": ">=17.0.0" jotai: ">=2.0.0" @@ -13534,7 +13534,7 @@ __metadata: optional: true react: optional: true - checksum: 67cbcd3ead992765b7a6f72e369f63da4a13519474c13a9d9f4acb35f2c205ee79c3e6ed0e698d17de6662f2c6cf97eb4e89486c6cd3e0d4f266b21094eaec1d + checksum: 1f42e706f9f7ba28ffd713623672ba46b00afb1b5ee62507948aeacf2f58a1b9074c30445757f387bae5a278050ec0b82b624a59aafdd732403c1d3b64eb87af languageName: node linkType: hard