From 89bea1083dca5014a1ddbd6e53a5df06d66c2e9b Mon Sep 17 00:00:00 2001 From: Shane Friedman Date: Fri, 24 Oct 2025 16:30:44 -0400 Subject: [PATCH] Support full state resets --- .yarn/versions/b0eed05a.yml | 2 ++ demo/main.tsx | 11 +++++++++- package.json | 4 ++-- src/hooks/useEditor.ts | 43 +++++++++++++++++++++---------------- src/hooks/useEffectEvent.ts | 16 ++++++++++++++ yarn.lock | 4 ++-- 6 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 .yarn/versions/b0eed05a.yml create mode 100644 src/hooks/useEffectEvent.ts diff --git a/.yarn/versions/b0eed05a.yml b/.yarn/versions/b0eed05a.yml new file mode 100644 index 0000000..766dd55 --- /dev/null +++ b/.yarn/versions/b0eed05a.yml @@ -0,0 +1,2 @@ +releases: + "@handlewithcare/react-codemirror": patch diff --git a/demo/main.tsx b/demo/main.tsx index d1f2edb..df0e1df 100644 --- a/demo/main.tsx +++ b/demo/main.tsx @@ -139,7 +139,16 @@ function DemoEditor() { dispatchTransactions={dispatchTransactions} extensions={extensions} > - +
+ + +
diff --git a/package.json b/package.json index f9173eb..0a6f038 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "peerDependencies": { "@codemirror/state": "*", "@codemirror/view": "*", - "react": ">=17 <=19", - "react-dom": ">=17 <=19" + "react": ">=18 <=19", + "react-dom": ">=18 <=19" } } diff --git a/src/hooks/useEditor.ts b/src/hooks/useEditor.ts index 7a3cfbe..19bd234 100644 --- a/src/hooks/useEditor.ts +++ b/src/hooks/useEditor.ts @@ -1,9 +1,12 @@ import { EditorState, type Transaction } from "@codemirror/state"; import { EditorView, type EditorViewConfig } from "@codemirror/view"; -import { useLayoutEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { tracking } from "../extensions/tracking.js"; +import { useClientLayoutEffect } from "./useClientLayoutEffect.js"; +import { useEffectEvent } from "./useEffectEvent.js"; + export type UseViewOptions = Omit & { defaultState?: EditorState; }; @@ -68,35 +71,37 @@ export function useEditor( const [view, setView] = useState(null); - useLayoutEffect(() => { + const createEditorView = useEffectEvent((parent: HTMLDivElement | null) => { + if (parent) { + return new EditorView({ + parent, + ...config, + }); + } + return null; + }); + + useClientLayoutEffect(() => { + const view = createEditorView(parent); + setView(view); + return () => { view?.destroy(); }; - }, [view]); + }, [parent, createEditorView]); - // eslint-disable-next-line react-hooks/exhaustive-deps - useLayoutEffect(() => { + useClientLayoutEffect(() => { dispatchTransactionsRef.current = dispatchTransactions; - if (!parent) { - setView(null); - return; - } - if (!view || view.dom.parentElement !== parent) { - const newView = new EditorView({ - parent, - ...config, - }); - setView(newView); - return; - } - const trs = state.field(tracking); const newTrs = trs.filter((tr) => !seen.current.has(tr)); - if (newTrs.length) { + if (view && newTrs.length) { view.update(newTrs); newTrs.forEach((tr) => seen.current.add(tr)); + } else if (view && view.state.doc.toString() !== state.doc.toString()) { + view.setState(state); + seen.current = new Set(trs); } }); diff --git a/src/hooks/useEffectEvent.ts b/src/hooks/useEffectEvent.ts new file mode 100644 index 0000000..d11c037 --- /dev/null +++ b/src/hooks/useEffectEvent.ts @@ -0,0 +1,16 @@ +import { useCallback, useInsertionEffect, useRef } from "react"; + +export function useEffectEvent

( + fn: (...args: P) => R, +): (...funcArgs: P) => R { + const ref = useRef(fn); + + useInsertionEffect(() => { + ref.current = fn; + }, [fn]); + + return useCallback((...args: P): R => { + const f = ref.current; + return f(...args); + }, []); +} diff --git a/yarn.lock b/yarn.lock index c93d54b..e4cd981 100644 --- a/yarn.lock +++ b/yarn.lock @@ -647,8 +647,8 @@ __metadata: peerDependencies: "@codemirror/state": "*" "@codemirror/view": "*" - react: ">=17 <=19" - react-dom: ">=17 <=19" + react: ">=18 <=19" + react-dom: ">=18 <=19" languageName: unknown linkType: soft