Skip to content

Commit dd80e8c

Browse files
committed
fix(react): update latest-ref callbacks at layout-effect timing
Review finding: the refs behind useEditorChange, useEditorSelectionChange and useEditorFocusChange were updated in a passive effect, so a layout effect firing an editor event right after commit could still reach the previous render's callback. The refs now update in an isomorphic layout effect — extracted from useEditorState, which already had the SSR-safe variant inline.
1 parent c230245 commit dd80e8c

5 files changed

Lines changed: 29 additions & 7 deletions

File tree

packages/react/src/hooks/useEditorChange.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { BlockNoteEditor } from "@blocknote/core";
22
import { useEffect, useRef } from "react";
3+
import { useIsomorphicLayoutEffect } from "../util/useIsomorphicLayoutEffect.js";
34
import { useBlockNoteContext } from "../editor/BlockNoteContext.js";
45

56
/**
@@ -23,7 +24,10 @@ export function useEditorChange(
2324
// Latest-ref pattern: the subscription lives as long as the editor does,
2425
// while the callback stays current without resubscribing on re-renders.
2526
const callbackRef = useRef(callback);
26-
useEffect(() => {
27+
// Layout-effect timing, not passive: a layout effect elsewhere can
28+
// trigger an editor event right after commit, and the subscription must
29+
// not invoke the previous render's callback then.
30+
useIsomorphicLayoutEffect(() => {
2731
callbackRef.current = callback;
2832
});
2933

packages/react/src/hooks/useEditorFocusChange.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { BlockNoteEditor } from "@blocknote/core";
22
import { useEffect, useRef } from "react";
3+
import { useIsomorphicLayoutEffect } from "../util/useIsomorphicLayoutEffect.js";
34
import { useBlockNoteContext } from "../editor/BlockNoteContext.js";
45

56
/**
@@ -29,7 +30,10 @@ export function useEditorFocusChange(
2930
// Latest-ref pattern: the subscription lives as long as the editor does,
3031
// while the callback stays current without retriggering the effect.
3132
const callbackRef = useRef(callback);
32-
useEffect(() => {
33+
// Layout-effect timing, not passive: a layout effect elsewhere can
34+
// trigger an editor event right after commit, and the subscription must
35+
// not invoke the previous render's callback then.
36+
useIsomorphicLayoutEffect(() => {
3337
callbackRef.current = callback;
3438
});
3539

packages/react/src/hooks/useEditorSelectionChange.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { BlockNoteEditor } from "@blocknote/core";
22
import { useEffect, useRef } from "react";
3+
import { useIsomorphicLayoutEffect } from "../util/useIsomorphicLayoutEffect.js";
34
import { useBlockNoteContext } from "../editor/BlockNoteContext.js";
45

56
/**
@@ -26,7 +27,10 @@ export function useEditorSelectionChange(
2627
// Latest-ref pattern: the subscription lives as long as the editor does,
2728
// while the callback stays current without resubscribing on re-renders.
2829
const callbackRef = useRef(callback);
29-
useEffect(() => {
30+
// Layout-effect timing, not passive: a layout effect elsewhere can
31+
// trigger an editor event right after commit, and the subscription must
32+
// not invoke the previous render's callback then.
33+
useIsomorphicLayoutEffect(() => {
3034
callbackRef.current = callback;
3135
});
3236

packages/react/src/hooks/useEditorState.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import type { BlockNoteEditor } from "@blocknote/core";
22
import deepEqual from "fast-deep-equal/es6/react.js";
3-
import { useDebugValue, useEffect, useLayoutEffect, useState } from "react";
3+
import { useDebugValue, useState } from "react";
44
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
55
import { useBlockNoteContext } from "../editor/BlockNoteContext.js";
6-
7-
const useIsomorphicLayoutEffect =
8-
typeof window !== "undefined" ? useLayoutEffect : useEffect;
6+
import { useIsomorphicLayoutEffect } from "../util/useIsomorphicLayoutEffect.js";
97

108
export type EditorStateSnapshot<
119
TEditor extends BlockNoteEditor<any, any, any> | null = BlockNoteEditor<
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useEffect, useLayoutEffect } from "react";
2+
3+
/**
4+
* `useLayoutEffect` in the browser, `useEffect` under SSR — where
5+
* `useLayoutEffect` cannot run and React warns.
6+
*
7+
* Used for latest-ref updates: the ref must be current before any layout
8+
* effect can trigger an editor event, or a subscription could still invoke
9+
* the previous render's callback.
10+
*/
11+
export const useIsomorphicLayoutEffect =
12+
typeof window !== "undefined" ? useLayoutEffect : useEffect;

0 commit comments

Comments
 (0)