forked from ianstormtaylor/slate
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/slate selectors (ianstormtaylor#4841)
* Added a redux inspired slate selector * added changeset
- Loading branch information
Showing
4 changed files
with
164 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'slate-react': minor | ||
--- | ||
|
||
Added redux-style useSlateSelector to improve and prevent unneccessary rerendering with the useSlate hook |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
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 | ||
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 selectedState | ||
} | ||
|
||
/** | ||
* Create selector context with editor updating on every editor change | ||
*/ | ||
export function getSelectorContext(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)) | ||
}, []) | ||
|
||
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 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters