tiptap.ts1.7 KBView on GitHub
/**
 * `editor` is not a sufficient liveness check.
 *
 * TipTap's `useEditor` does not destroy an editor when its component unmounts — it schedules
 * the destroy on a 1ms timer (`EditorInstanceManager.scheduleDestroy`), so the teardown lands
 * a tick later, after React has already finished the commit. And a destroyed editor is NOT
 * null: it keeps its identity while `commandManager`, `view` and `state` are nulled out
 * underneath it.
 *
 * So `if (!editor) return` is a half-guard. Anything still holding the reference when the
 * destroy lands — an effect re-running because a query resolved mid-teardown, a debounce
 * timer, a callback resuming after `await`, a parent that stashed the editor from
 * `onEditorCreated` — reaches a live-LOOKING object and throws on the first property it
 * touches: `Cannot read properties of null (reading 'commands')` from `editor.commands`,
 * `(reading 'nodes')` from `editor.getText()`. Those two were the top production errors,
 * and because they throw from render/effect they escape to the route error boundary and
 * take the whole page down rather than failing quietly.
 *
 * Use this anywhere the editor is touched outside a direct user interaction on a mounted
 * component: effects, timers, microtasks, and anything after an `await`.
 *
 *   liveEditor(editor)?.commands.setContent(markdown);
 *
 * Structurally typed rather than tied to `Editor` so it works for both the `@tiptap/react`
 * and `@tiptap/core` editor types, and for the handles components expose over them.
 */
export function liveEditor<T extends { isDestroyed: boolean }>(
  editor: T | null | undefined,
): T | null {
  return editor && !editor.isDestroyed ? editor : null;
}