diff --git a/.changeset/angry-files-kick.md b/.changeset/angry-files-kick.md new file mode 100644 index 0000000000..c687a96350 --- /dev/null +++ b/.changeset/angry-files-kick.md @@ -0,0 +1,7 @@ +--- +'slate-react': minor +--- + +- Introduces a `useSlateSelection` hook that triggers whenever the selection changes. +- This also changes the implementation of SlateContext to use an incrementing value instead of an array replace to trigger updates +- Introduces a `useSlateWithV` hook that includes the version counter which can be used to prevent re-renders diff --git a/docs/libraries/slate-react.md b/docs/libraries/slate-react.md index 3945d3d7a1..3851163ba4 100644 --- a/docs/libraries/slate-react.md +++ b/docs/libraries/slate-react.md @@ -96,10 +96,18 @@ Get the current `selected` state of an element. Get the current editor object from the React context. Re-renders the context whenever changes occur in the editor. +### `useSlateWithV` + +The same as `useSlate()` but includes a version counter which you can use to prevent re-renders. + ### `useSlateStatic` Get the current editor object from the React context. A version of useSlate that does not re-render the context. Previously called `useEditor`. +### `useSlateSelection` + +Get the current editor selection from the React context. Only re-renders when the selection changes. + ## ReactEditor A React and DOM-specific version of the `Editor` interface. All about translating between the DOM and Slate. diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index ea80c68149..63a964daf6 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -150,7 +150,7 @@ export const Editable = (props: EditableProps) => { [] ) - // Whenever the editor updates... + // Whenever the editor updates, sync the DOM selection with the slate selection useIsomorphicLayoutEffect(() => { // Update element-related weak maps with the DOM element ref. let window diff --git a/packages/slate-react/src/components/slate.tsx b/packages/slate-react/src/components/slate.tsx index 9e21a9d61b..a4e4088e81 100644 --- a/packages/slate-react/src/components/slate.tsx +++ b/packages/slate-react/src/components/slate.tsx @@ -3,7 +3,7 @@ import { Editor, Node, Descendant, Scrubber } from 'slate' import { ReactEditor } from '../plugin/react-editor' import { FocusedContext } from '../hooks/use-focused' import { EditorContext } from '../hooks/use-slate-static' -import { SlateContext } from '../hooks/use-slate' +import { SlateContext, SlateContextValue } from '../hooks/use-slate' import { getSelectorContext, SlateSelectorContext, @@ -26,7 +26,7 @@ export const Slate = (props: { const { editor, children, onChange, value, ...rest } = props const unmountRef = useRef(false) - const [context, setContext] = React.useState<[ReactEditor]>(() => { + const [context, setContext] = React.useState(() => { if (!Node.isNodeList(value)) { throw new Error( `[Slate] value is invalid! Expected a list of elements` + @@ -41,7 +41,7 @@ export const Slate = (props: { } editor.children = value Object.assign(editor, rest) - return [editor] + return { v: 0, editor } }) const { @@ -54,7 +54,10 @@ export const Slate = (props: { onChange(editor.children) } - setContext([editor]) + setContext(prevContext => ({ + v: prevContext.v + 1, + editor, + })) handleSelectorChange(editor) }, [onChange]) diff --git a/packages/slate-react/src/hooks/use-slate-selection.tsx b/packages/slate-react/src/hooks/use-slate-selection.tsx new file mode 100644 index 0000000000..1a936b90c9 --- /dev/null +++ b/packages/slate-react/src/hooks/use-slate-selection.tsx @@ -0,0 +1,17 @@ +import { BaseSelection, Range } from 'slate' + +import { useSlateSelector } from './use-slate-selector' + +/** + * Get the current slate selection. + * Only triggers a rerender when the selection actually changes + */ +export const useSlateSelection = () => { + return useSlateSelector(editor => editor.selection, isSelectionEqual) +} + +const isSelectionEqual = (a: BaseSelection, b: BaseSelection) => { + if (!a && !b) return true + if (!a || !b) return false + return Range.equals(a, b) +} diff --git a/packages/slate-react/src/hooks/use-slate.tsx b/packages/slate-react/src/hooks/use-slate.tsx index ffb6de555f..ff493588cb 100644 --- a/packages/slate-react/src/hooks/use-slate.tsx +++ b/packages/slate-react/src/hooks/use-slate.tsx @@ -7,7 +7,15 @@ import { ReactEditor } from '../plugin/react-editor' * context whenever changes occur. */ -export const SlateContext = createContext<[ReactEditor] | null>(null) +export interface SlateContextValue { + v: number + editor: ReactEditor +} + +export const SlateContext = createContext<{ + v: number + editor: ReactEditor +} | null>(null) /** * Get the current editor object from the React context. @@ -22,6 +30,18 @@ export const useSlate = (): Editor => { ) } - const [editor] = context + const { editor } = context return editor } + +export const useSlateWithV = () => { + const context = useContext(SlateContext) + + if (!context) { + throw new Error( + `The \`useSlate\` hook must be used inside the component's context.` + ) + } + + return context +} diff --git a/packages/slate-react/src/index.ts b/packages/slate-react/src/index.ts index f652ddd598..8a96651eaa 100644 --- a/packages/slate-react/src/index.ts +++ b/packages/slate-react/src/index.ts @@ -23,8 +23,9 @@ export { useSlateStatic } from './hooks/use-slate-static' export { useFocused } from './hooks/use-focused' export { useReadOnly } from './hooks/use-read-only' export { useSelected } from './hooks/use-selected' -export { useSlate } from './hooks/use-slate' +export { useSlate, useSlateWithV } from './hooks/use-slate' export { useSlateSelector } from './hooks/use-slate-selector' +export { useSlateSelection } from './hooks/use-slate-selection' // Plugin export { ReactEditor } from './plugin/react-editor'