React Forum
Blog & Guide Responses
Jan 17, 2023 · 04:00 PM

[Guide] Build a Markdown Editor - implementation notes

Started by Priya Liao
guidemarkdownFieldmulti-truthderived previewrouting

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?

Read the guide

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 latest
Keira Santos
Jan 17, 2023 · 04:08 PM

If 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?

Noah Vance
Jan 17, 2023 · 04:14 PM

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.

Keira Santos
Jan 17, 2023 · 04:17 PM

Ok, so mode toggle is basically a forced commit boundary.

Ari Novak
Jan 17, 2023 · 04:22 PM

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.

Camille Duarte
Jan 17, 2023 · 04:33 PM

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.

Priya Liao
Jan 17, 2023 · 04:36 PM

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.

Noah Vance
Jan 17, 2023 · 04:45 PM

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.

Keira Santos
Jan 17, 2023 · 04:57 PM

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).

Ari Novak
Jan 17, 2023 · 05:05 PM

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}`} />
Camille Duarte
Jan 17, 2023 · 05:18 PM

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.

Priya Liao
Jan 17, 2023 · 05:25 PM

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.

Noah Vance
Jan 17, 2023 · 05:41 PM

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.

Dmitri Kline
Jan 17, 2023 · 06:02 PM

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.

Salma Qureshi
Jan 17, 2023 · 06:11 PM

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.

Jules Kwon
Jan 17, 2023 · 06:25 PM

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).

Lina Ortega
Jan 17, 2023 · 06:39 PM

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.