Skip to content

Commit

Permalink
chore: update selectors, decorations to use new useSyncExternalStore()
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonphillips committed May 30, 2022
1 parent 6f835cb commit 19a0abc
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 156 deletions.
4 changes: 3 additions & 1 deletion packages/slate-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
"dependencies": {
"@types/is-hotkey": "^0.1.1",
"@types/lodash": "^4.14.149",
"@types/use-sync-external-store": "^0.0.3",
"direction": "^1.0.3",
"is-hotkey": "^0.1.6",
"is-plain-object": "^5.0.0",
"lodash": "^4.17.4",
"scroll-into-view-if-needed": "^2.2.20",
"tiny-invariant": "1.0.6"
"tiny-invariant": "1.0.6",
"use-sync-external-store": "^1.1.0"
},
"devDependencies": {
"@babel/runtime": "^7.7.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/slate-react/src/hooks/use-children.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const useChildren = (props: {
const n = node.children[i] as Descendant
const key = ReactEditor.findKey(editor, n)
const range = Editor.range(editor, p)
const sel = selection && Range.intersection(range, selection)
const sel = selection && Range.intersection(selection, range)
const ds = []

for (const dec of decorations) {
Expand Down
90 changes: 11 additions & 79 deletions packages/slate-react/src/hooks/use-decorations.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useReducer,
useRef,
} from 'react'
import { createContext, useCallback, useContext, useMemo, useRef } from 'react'
import { Node, NodeEntry, BaseRange } from 'slate'
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
import { ReactEditor } from '..'
import { useSlateStatic } from './use-slate-static'
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'
import { isDecoratorRangeListEqual } from '../utils/range-list'

function isError(error: any): error is Error {
return error instanceof Error
}

type DecorationsList = (BaseRange & { placeholder?: string | undefined })[]
type DecorateFn = (entry: NodeEntry) => DecorationsList
type DecorateChangeHandler = (decorate: DecorateFn) => void
Expand All @@ -29,11 +18,7 @@ export const DecorateContext = createContext<{
addEventListener: (callback: DecorateChangeHandler) => () => void
}>({} as any)

/**
* use redux style selectors to prevent rerendering on every decorate change
*/
export function useDecorations(node: Node) {
const [iteration, forceRender] = useReducer(s => s + 1, 0)
const editor = useSlateStatic()
const context = useContext(DecorateContext)

Expand All @@ -44,71 +29,18 @@ export function useDecorations(node: Node) {
}
const { getDecorate, addEventListener } = context

const latestSubscriptionCallbackError = useRef<Error | undefined>()
const latestNode = useRef<Node>(node)
const latestDecorationState = useRef<DecorationsList>([])
let decorationState: DecorationsList

try {
if (
iteration === 0 ||
node !== latestNode.current ||
latestSubscriptionCallbackError.current
) {
const path = ReactEditor.findPath(editor, node)
decorationState = getDecorate()([node, path])
} else {
decorationState = latestDecorationState.current
}
} catch (err) {
if (latestSubscriptionCallbackError.current && isError(err)) {
err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
}

throw err
const getDecorations = (decorate: DecorateFn) => {
const path = ReactEditor.findPath(editor, node)
return decorate([node, path])
}
useIsomorphicLayoutEffect(() => {
latestNode.current = node
latestDecorationState.current = decorationState
latestSubscriptionCallbackError.current = undefined
})

useIsomorphicLayoutEffect(
() => {
function checkForUpdates() {
try {
const path = ReactEditor.findPath(editor, latestNode.current)
const newDecorationState = getDecorate()([latestNode.current, path])

if (
isDecoratorRangeListEqual(
newDecorationState,
latestDecorationState.current
)
) {
return
}

latestDecorationState.current = newDecorationState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err
}

forceRender()
}

const unsubscribe = addEventListener(checkForUpdates)
return () => unsubscribe()
},
// don't rerender on equalityFn change since we want to be able to define it inline
[addEventListener, getDecorate]
return useSyncExternalStoreWithSelector(
addEventListener,
getDecorate,
null,
getDecorations,
isDecoratorRangeListEqual
)

return decorationState
}

/**
Expand Down
84 changes: 10 additions & 74 deletions packages/slate-react/src/hooks/use-slate-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useReducer,
useRef,
} from 'react'
import { createContext, useCallback, useContext, useMemo, useRef } from 'react'
import { Editor } from 'slate'
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'

function isError(error: any): error is Error {
return error instanceof Error
}
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'

type EditorChangeHandler = (editor: Editor) => void

/**
* A React context for sharing the editor selector context in a way to control rerenders
*/
Expand All @@ -26,7 +16,7 @@ export const SlateSelectorContext = createContext<{
const refEquality = (a: any, b: any) => a === b

/**
* use redux style selectors to prevent rerendering on every keystroke.
* use useSyncExternalStore to prevent rerendering on every keystroke.
* Bear in mind rerendering can only prevented if the returned value is a value type or for reference types (e.g. objects and arrays) add a custom equality function.
*
* Example:
Expand All @@ -38,7 +28,6 @@ export function useSlateSelector<T>(
selector: (editor: Editor) => T,
equalityFn: (a: T, b: T) => boolean = refEquality
) {
const [, forceRender] = useReducer(s => s + 1, 0)
const context = useContext(SlateSelectorContext)
if (!context) {
throw new Error(
Expand All @@ -47,66 +36,13 @@ export function useSlateSelector<T>(
}
const { getSlate, addEventListener } = context

const latestSubscriptionCallbackError = useRef<Error | undefined>()
const latestSelector = useRef<(editor: Editor) => T>(() => null as any)
const latestSelectedState = useRef<T>((null as any) as T)
let selectedState: T

try {
if (
selector !== latestSelector.current ||
latestSubscriptionCallbackError.current
) {
selectedState = selector(getSlate())
} else {
selectedState = latestSelectedState.current
}
} catch (err) {
if (latestSubscriptionCallbackError.current && isError(err)) {
err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
}

throw err
}
useIsomorphicLayoutEffect(() => {
latestSelector.current = selector
latestSelectedState.current = selectedState
latestSubscriptionCallbackError.current = undefined
})

useIsomorphicLayoutEffect(
() => {
function checkForUpdates() {
try {
const newSelectedState = latestSelector.current(getSlate())

if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}

latestSelectedState.current = newSelectedState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err
}

forceRender()
}

const unsubscribe = addEventListener(checkForUpdates)

checkForUpdates()

return () => unsubscribe()
},
// don't rerender on equalityFn change since we want to be able to define it inline
[addEventListener, getSlate]
return useSyncExternalStoreWithSelector(
addEventListener,
getSlate,
null,
selector,
equalityFn
)

return selectedState
}

/**
Expand Down
10 changes: 9 additions & 1 deletion packages/slate-react/test/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React from 'react'
import { createEditor, NodeEntry, Range, Element, Transforms } from 'slate'
import { create, act, ReactTestRenderer } from 'react-test-renderer'
import { Slate, withReact, DefaultEditable } from '../src'
import {
Slate,
withReact,
DefaultEditable,
RenderElementProps,
DefaultElement,
RenderLeafProps,
DefaultLeaf,
} from '../src'

const createNodeMock = () => ({
ownerDocument: global.document,
Expand Down
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3748,6 +3748,13 @@ __metadata:
languageName: node
linkType: hard

"@types/use-sync-external-store@npm:^0.0.3":
version: 0.0.3
resolution: "@types/use-sync-external-store@npm:0.0.3"
checksum: 161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e
languageName: node
linkType: hard

"@types/yargs-parser@npm:*":
version: 20.2.1
resolution: "@types/yargs-parser@npm:20.2.1"
Expand Down Expand Up @@ -14636,6 +14643,7 @@ resolve@^2.0.0-next.3:
"@types/react": ^16.9.13
"@types/react-dom": ^16.9.4
"@types/react-test-renderer": ^16.8.0
"@types/use-sync-external-store": ^0.0.3
direction: ^1.0.3
is-hotkey: ^0.1.6
is-plain-object: ^5.0.0
Expand All @@ -14648,6 +14656,7 @@ resolve@^2.0.0-next.3:
slate-hyperscript: ^0.77.0
source-map-loader: ^0.2.4
tiny-invariant: 1.0.6
use-sync-external-store: ^1.1.0
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
Expand Down Expand Up @@ -16331,6 +16340,15 @@ typescript@3.9.7:
languageName: node
linkType: hard

"use-sync-external-store@npm:^1.1.0":
version: 1.1.0
resolution: "use-sync-external-store@npm:1.1.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 8993a0b642f91d7fcdbb02b7b3ac984bd3af4769686f38291fe7fcfe73dfb73d6c64d20dfb7e5e7fbf5a6da8f5392d6f8e5b00c243a04975595946e82c02b883
languageName: node
linkType: hard

"use@npm:^3.1.0":
version: 3.1.1
resolution: "use@npm:3.1.1"
Expand Down

0 comments on commit 19a0abc

Please sign in to comment.