forked from ianstormtaylor/slate
-
Notifications
You must be signed in to change notification settings - Fork 1
/
use-slate-selector.tsx
148 lines (130 loc) · 4.19 KB
/
use-slate-selector.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import {
createContext,
useCallback,
useContext,
useMemo,
useReducer,
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
}
type EditorChangeHandler = (editor: Editor) => void
/**
* A React context for sharing the editor selector context in a way to control rerenders
*/
export const SlateSelectorContext = createContext<{
getSlate: () => Editor
addEventListener: (callback: EditorChangeHandler) => () => void
}>({} as any)
const refEquality = (a: any, b: any) => a === b
/**
* use redux style selectors 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:
* ```
* const isSelectionActive = useSlateSelector(editor => Boolean(editor.selection));
* ```
*/
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(
`The \`useSlateSelector\` hook must be used inside the <Slate> component's context.`
)
}
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
if (err instanceof Error) {
latestSubscriptionCallbackError.current = err
} else {
latestSubscriptionCallbackError.current = new Error(String(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 selectedState
}
/**
* Create selector context with editor updating on every editor change
*/
export function useSelectorContext(editor: Editor) {
const eventListeners = useRef<EditorChangeHandler[]>([]).current
const slateRef = useRef<{
editor: Editor
}>({
editor,
}).current
const onChange = useCallback(
(editor: Editor) => {
slateRef.editor = editor
eventListeners.forEach((listener: EditorChangeHandler) =>
listener(editor)
)
},
[eventListeners, slateRef]
)
const selectorContext = useMemo(() => {
return {
getSlate: () => slateRef.editor,
addEventListener: (callback: EditorChangeHandler) => {
eventListeners.push(callback)
return () => {
eventListeners.splice(eventListeners.indexOf(callback), 1)
}
},
}
}, [eventListeners, slateRef])
return { selectorContext, onChange }
}