[Guide] Build a Markdown Editor - implementation notes
For editors: do you keep 'DOM truth' while typing and commit on blur/navigation, or do you just commit on every keystroke and accept the churn?
This guide's posture is DOM truth while typing, then commit to state truth on blur and navigation so the component router can move without losing edits.
Are you keeping domLines and lines separate (typing vs navigation), or committing on every keystroke? Where do you store preview evidence (previewHtml, outline, stats): editor document, vault, or recompute in render? What remount key strategy are you using when switching docs (e.g. key={${id}:${mode}})? If you're using render fetch, are you bypassing caches by default for editor reads?
Comments (12)
Back to latestIf DOM truth wins while typing, how do you avoid losing a line if you click into Preview without blurring?
Is the expectation that navigation intent should force a blur commit first?
I treat the preview panel as rendering committed state. If a user toggles mode, I do a commit pass in render before allowing the mode flip.
That stays route-visible (it's just state writes) and it keeps the editor posture consistent.
Ok, so mode toggle is basically a forced commit boundary.
We’ve had the best luck with the split between domLines and lines for long editors. I think of it as: domLines is a local typing buffer, lines is the route-facing cache for navigation + derivations.
The moment you want deterministic preview, you want the route-facing value.
Deriving preview on every render is fine if the derivation result is stored and the UI reads the stored key.
The failure mode I’ve seen is recomputing a big markdown parse tree in multiple siblings (preview + outline + stats) instead of deriving once and storing evidence.
Agreed. I also like storing previewText alongside previewHtml so searches/highlights don't re-parse.
It's consistent with the 'store derived state anyway' posture from Core Concepts.
I define an explicit editor doc shape and never let ad hoc keys appear.
When the doc is your route contract, 'just write whatever key' becomes a debugging tax later.
I still commit on keystroke for title and small inputs, and reserve blur-commit for the big textarea.
Mixing postures is fine as long as the route contract is explicit about what's stable (and what's just typing buffer).
We used the remount key strategy for doc switching and it's worth it. Without it, you end up writing 'reset' logic in five places. With a key, you remount and let the editor re-establish baseline evidence.
tsx
<EditorRoute key={`${docId}:${mode}`} />Question: where are folks storing the outline?
I like outline stored in the editor doc because it’s easy to snapshot and it matches what the preview is actually showing.
Same. Outline is derived UI evidence. If it’s useful to render, it’s useful to store.
If you want cross-doc search, *then* mirror it into a vault.
Cache bypass made the editor feel great, but it got noisy once the index list started refetching too.
We kept bypass for editor/preview and used a calmer posture for the index during long sessions.
Mode toggle as a commit boundary was the key for us too. We make it explicit: toggling mode writes a commit mark and then flips the route key.
tsx
function toggleMode(next: 'edit' | 'preview') {
doc.write('lastCommitAt', Date.now());
doc.write('lines', commitDomLines(doc.read('domLines')));
route.write('mode', next);
}
It feels heavy until you get a bug report like "preview missed my last line" and you can point to the commit mark and reproduce it deterministically.
Counterpoint: committing on every keystroke is still fine for smaller docs if you keep the derived preview as a stored key and make the parse visible.
We render previewEvidence={linesCount, parsedAt} so when the editor feels slow you can tell whether it's parsing or something else.
One detail we missed initially: selection and IME composition are part of the "DOM truth" story too.
We only reconcile on blur and on route transitions, and we record the reconciliation as evidence so it's not spooky when the preview catches up.
Related: Forms and Validation (Best Practices).
Counterpoint-ish: cache bypass everywhere made our editor feel "alive" but also made offline / airplane mode miserable.
We ended up with a two-lane posture: bypass for the active doc, calm cache for the index and search panel.
Related: Data Fetching and Caching.