Skip to content

Instantly share code, notes, and snippets.

@BrianHung
Created April 29, 2022 09:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BrianHung/f2b9b5de6b05de222cbacaf13745eb3c to your computer and use it in GitHub Desktop.
Save BrianHung/f2b9b5de6b05de222cbacaf13745eb3c to your computer and use it in GitHub Desktop.
ProseMirror and Granular Updates with React useSyncExternalStore
import React, { createContext, useContext, useState, useRef } from "react"
import { EditorView, EditorProps } from "prosemirror-view"
import { useSyncExternalStore } from "use-sync-external-store/shim"
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector"
import { memo, useEffect } from "react"
import { useEditorContext } from "./hooks/useEditor"
export type ReactEditorProps = Partial<EditorProps> & {
className?: string;
}
export const Editor: React.FC<ReactEditorProps> = memo((props) => {
const {
className = ""
} = props
const {setEditor, ReactSyncExternalStorePlugin} = useEditorContext()
const editorRef = useRef<HTMLDivElement>(null)
useEffect(
function initEditor() {
setEditor(new EditorView({
...props,
place: {mount: editorRef.current},
plugins: props.plugins?.concat(ReactSyncExternalStorePlugin),
}))
},
[],
)
return (
<div
className={className}
ref={editorRef}
/>
)
})
export default Editor
import React, { createContext, useContext, useMemo, useState } from "react"
import { EditorView } from "prosemirror-view"
import { useSyncExternalStore } from "use-sync-external-store/shim"
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector"
import { EditorState, Plugin, PluginKey } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
const ReactSyncExternalStoreKey = new PluginKey("ReactSyncExternalStore")
export const EditorContext = createContext<{
editor: EditorView,
setEditor: ((editor: EditorView) => void)
listeners: Set<EditorListener>
subscribe: ((listener: EditorListener) => Function)
ReactSyncExternalStorePlugin: Plugin
}>(undefined)
EditorContext.displayName = 'EditorContext'
export const useEditorContext = () => {
const context = useContext(EditorContext);
if (!context) {
throw Error(
"EditorContext is not defined. Did you forget to wrap your component in an EditorProvider?"
)
}
return context;
}
const defaultViewSelector = (view: EditorView) => view
/**
* Alternatively called `useEditorView`.
*/
export const useEditor = (
selector: (view: EditorView) => any = defaultViewSelector,
isEqual?: (view: EditorView) => boolean,
) => {
const context = useEditorContext()
return useSyncExternalStoreWithSelector(
context.subscribe,
() => context.editor,
null,
selector,
isEqual,
)
}
const defaultStateSelector = (state: EditorState) => state
export type EditorListener = (view: EditorView, prevState?: EditorState) => void
export const useEditorState = (
selector: (state: EditorState) => any = defaultStateSelector,
isEqual?: (state: EditorState) => boolean,
) => {
const context = useEditorContext()
return useSyncExternalStoreWithSelector(
context.subscribe,
() => context.editor?.state,
null,
selector,
isEqual,
)
}
export const EditorProvider = ({children}: {children: React.ReactNode}) => {
const [editor, setEditor] = useState<Editor|undefined>(undefined)
const [listeners, subscribe, ReactSyncExternalStorePlugin] = useMemo(
() => {
const listeners = new Set<EditorListener>();
const subscribe = (listener: EditorListener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
/**
* Direct plugin that updates all editor selectors.
* https://prosemirror.net/docs/ref/#view.DirectEditorProps.plugins
*/
const ReactSyncExternalStorePlugin = new Plugin({
key: ReactSyncExternalStoreKey,
view: view => ({
update(view, prevState) {
listeners.forEach(l => l(view, prevState));
},
destroy() {
listeners.forEach(l => l(view, undefined));
}
})
});
return [
listeners,
subscribe,
ReactSyncExternalStorePlugin
]
},
[]
)
return (
<EditorContext.Provider value={{
editor,
setEditor,
listeners,
subscribe,
ReactSyncExternalStorePlugin
}}>
{children}
</EditorContext.Provider>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment