Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions src/components/editor/DocumentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -910,11 +910,8 @@ export function DocumentEditor({
},
}),
],
...(contentType === "markdown" && typeof content === "string"
? { content, contentType: "markdown" as const }
: content
? { content }
: {}),
// NOTE: initial content intentionally omitted — content is set via the
// useEffect below after first paint to avoid blocking modal-open animation.
onUpdate: ({ editor }) => {
const md =
typeof editor.getMarkdown === "function" ? editor.getMarkdown() : "";
Expand All @@ -930,11 +927,25 @@ export function DocumentEditor({
},
});

// Sync incoming content only when it actually differs from the live editor state.
const hasLoadedInitialContentRef = useRef(false);
const autofocusRef = useRef(autofocus);
autofocusRef.current = autofocus;

// Reset the initial-load flag when the editor instance itself changes so a
// recreated editor still benefits from the deferred-paint optimization.
useEffect(() => {
hasLoadedInitialContentRef.current = false;
}, [editor]);

// Sync incoming content only when it actually differs from the live editor
// state. The first content load is deferred to after the modal's open paint
// to avoid blocking the open animation with markdown parsing + KaTeX renders.
useEffect(() => {
if (!editor) return;

queueMicrotask(() => {
const applyContent = () => {
if (editor.isDestroyed) return;

if (contentType === "markdown" && typeof content === "string") {
if (editor.getMarkdown?.() === content) return;
editor.commands.setContent(content, {
Expand All @@ -949,7 +960,34 @@ export function DocumentEditor({
return;

editor.commands.setContent(nextContent, { emitUpdate: false });
};

if (hasLoadedInitialContentRef.current) {
// External update (e.g., ZeroDB sync from another tab). Apply immediately.
applyContent();
return;
}
Comment on lines +965 to +969
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 hasLoadedInitialContentRef not reset on editor recreation

hasLoadedInitialContentRef is a component-scoped useRef and persists for the lifetime of the component instance. If TipTap's useEditor ever recreates the editor object (e.g. because an extension config prop changes), the effect runs with hasLoadedInitialContentRef.current === true and applies content synchronously on the fresh editor. In most cases this is harmless, but it also means the deferred-paint optimisation is silently bypassed for any editor rebuild. Adding a reset when editor changes would make the behaviour explicit:

useEffect(() => {
  hasLoadedInitialContentRef.current = false;
}, [editor]);

Fix in Cursor


// Initial mount: defer content load until after the modal's open paint to
// avoid blocking the animation. Two rAFs ensures we run after at least one
// committed frame is on screen.
let rafA: number | null = null;
let rafB: number | null = null;
rafA = requestAnimationFrame(() => {
rafB = requestAnimationFrame(() => {
if (editor.isDestroyed) return;
applyContent();
hasLoadedInitialContentRef.current = true;
if (autofocusRef.current) {
editor.commands.focus("end");
}
});
});

return () => {
if (rafA != null) cancelAnimationFrame(rafA);
if (rafB != null) cancelAnimationFrame(rafB);
};
}, [editor, content, contentType]);

return (
Expand Down
Loading