Definition
This is the architecture you want when building a block-based document editor like Notion, where your app holds ownership of the document data while the heavy rich-text editing engine is briefly attached and detached "on only the single block where the cursor currently sits." The key is not entrusting the whole document to a single editor.
A Focused guest editor (translator's note: there is no such standard term; it is a placeholder coined for this article) is an approach where the document's semantics — its block tree (the hierarchical structure of blocks), patches (change history), and permissions — are owned by the host (the actual owner of the document data = your app code), while the rich-text engine is a guest briefly mounted on only the currently focused text block. The guest talks to the host only through narrow channels: inject (the host feeds in the block's current value) / commit (the edited content is returned to the host) / keymap (keyboard shortcut mapping) delegation. It is the exact opposite of the common model where the whole document is one editor doc.
Why it matters
If you keep the whole document as a single rich-text document, the block tree·change history·permissions·real-time subscriptions all become bound to and dependent on the editor's projection (the internal document representation the editor builds). That makes it hard to attach per-block permissions, widgets outside blocks, or gestures spanning multiple blocks. If the host is the source of truth (the single authority for this data) and the guest holds only a focus lease (an editing permission borrowed briefly), you can keep the inline-editing feel while leaving the document's semantics in your domain model.
How it works
- The host holds the block array and the patch-apply gateway. Unfocused blocks are statically rendered (or read-only).
- When the user focuses a text block, a guest mounts on that block alone, and the host injects the current
content. - Typing·marks are handled inside the guest session. Structural operations (type change, split, move) are delegated to host callbacks.
- On focus loss·eviction, the guest returns its content to the host via commit and unmounts.
- To prevent a late blur commit right after split/merge from overwriting the new state, you need commit-before-delegate and lease retire before structural operations.
| Item | Description | Caveat |
|---|---|---|
| Host SoT | tree·patches·permissions·autosave | guest internal doc ≠ whole doc |
| Focus lease | guest on focused block only | commit required on unmount |
| Comm channels | inject / commit / keymap delegation | no mutation outside the channel |
| Stale commit | late blur overwrites a later patch | retire·commit-before-delegate |
| 3-tier key routing | text=guest, structure=host, no-focus=selection | mixing modes causes key clashes |
Practical application
Cross-block text moves cannot be solved with the guest's native DnD, since the guest owns only one block. Listen for HTML5 DnD at the container and reflect it via host patches. If you put the focus store outside React, subscribe with useSyncExternalStore and obey the snapshot-identity rule.
Trade-offs
- Regardless of block count, there is one (or a few) guest instance, so cost is predictable. In exchange, each focus switch incurs mount/commit cost and race handling.
- Keeping the host/guest boundary makes testing·permissions clear, but it becomes hard to use, as-is, library features built for a "whole document is one editor" API.
When not to use it
- When the document is a short single field (a one-line comment) — the guest/host split is overkill.
- When a collaborative CRDT already treats the whole document as one shared type and is the SoT — forcibly splitting the boundary only increases synchronization cost.
Common mistakes
- Mounting a guest on unfocused blocks too, for "convenience," causing performance·focus races.
- Not guaranteeing the order of blur commit and structural patch, causing stale overwrite.
- Mixing text keys and structural keys in one keymap, so shortcuts like Mod-D break differently per mode.
- Trying to implement cross-block moves with guest DnD and failing.
Related concepts
- two-stack-inverse-undo — layering the host document history and the guest session history.
- usesyncexternalstore-snapshot-identity — snapshot identity when keeping the focus/layout store as external state.
- html5-drag-and-drop — the channel for text moves crossing the guest boundary.