From 91659ad56b43e900353ebd8370bfcfc24f7adefb Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 17 Feb 2026 17:56:57 +0100 Subject: [PATCH] fix: editor content not displaying after single-instance refactor The single BlockNote editor instance was calling replaceBlocks before the editor was mounted in the DOM, causing content swaps to silently fail. Now tracks mount state via editor.onMount() and defers content swaps until the editor is ready. Also guards against stale async parses when the user switches tabs before markdown parsing completes. Co-Authored-By: Claude Opus 4.6 --- src/App.test.tsx | 1 + src/components/Editor.test.tsx | 1 + src/components/Editor.tsx | 42 +++++++++++++++++++++++++++------- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index fdb1f84d..76beef7a 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -77,6 +77,7 @@ vi.mock('@blocknote/react', () => ({ replaceBlocks: () => {}, document: [], insertInlineContent: () => {}, + onMount: (cb: () => void) => { cb(); return () => {} }, }), SuggestionMenuController: () => null, })) diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx index 24165354..dcce96a9 100644 --- a/src/components/Editor.test.tsx +++ b/src/components/Editor.test.tsx @@ -19,6 +19,7 @@ vi.mock('@blocknote/react', () => ({ replaceBlocks: () => {}, document: [], insertInlineContent: () => {}, + onMount: (cb: () => void) => { cb(); return () => {} }, }), SuggestionMenuController: () => null, })) diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 8695a9e5..a7bda22f 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -163,6 +163,21 @@ export const Editor = memo(function Editor({ // Cache parsed blocks per tab path for instant switching const tabCacheRef = useRef>(new Map()) const prevActivePathRef = useRef(null) + const editorMountedRef = useRef(false) + const pendingSwapRef = useRef<(() => void) | null>(null) + + // Track editor mount state + useEffect(() => { + const cleanup = editor.onMount(() => { + editorMountedRef.current = true + // Execute any pending content swap that was queued before mount + if (pendingSwapRef.current) { + pendingSwapRef.current() + pendingSwapRef.current = null + } + }) + return cleanup + }, [editor]) // Swap document content when active tab changes useEffect(() => { @@ -170,7 +185,7 @@ export const Editor = memo(function Editor({ const prevPath = prevActivePathRef.current // Save current editor state for the tab we're leaving - if (prevPath && prevPath !== activeTabPath) { + if (prevPath && prevPath !== activeTabPath && editorMountedRef.current) { cache.set(prevPath, editor.document) } prevActivePathRef.current = activeTabPath @@ -180,23 +195,34 @@ export const Editor = memo(function Editor({ const tab = tabs.find(t => t.entry.path === activeTabPath) if (!tab) return + const applyBlocks = (blocks: any[]) => { + editor.replaceBlocks(editor.document, blocks) + } + try { if (cache.has(activeTabPath)) { // Instant switch — use cached blocks - editor.replaceBlocks(editor.document, cache.get(activeTabPath)!) + const cached = cache.get(activeTabPath)! + if (editorMountedRef.current) { + applyBlocks(cached) + } else { + pendingSwapRef.current = () => applyBlocks(cached) + } } else { // First open — parse markdown const [, body] = splitFrontmatter(tab.content) const preprocessed = preProcessWikilinks(body) + const targetPath = activeTabPath editor.tryParseMarkdownToBlocks(preprocessed).then(blocks => { const withWikilinks = injectWikilinks(blocks) - try { - editor.replaceBlocks(editor.document, withWikilinks) - } catch (err) { - console.error('Failed to replace blocks:', err) - return + // Guard: skip if user switched tabs while we were parsing + if (prevActivePathRef.current !== targetPath) return + cache.set(targetPath, withWikilinks) + if (editorMountedRef.current) { + applyBlocks(withWikilinks) + } else { + pendingSwapRef.current = () => applyBlocks(withWikilinks) } - cache.set(activeTabPath, withWikilinks) }) } } catch (err) {