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

[Guide] Build a Recipe Manager - implementation notes

Started by Rowan Pierce
guiderecipesmutation without invalidationuseRenderFetchduplicate readsoptimistic writesdraftKeystored derived shopping lines

The Recipe Manager guide has a very specific posture: do optimistic mutation into a route document, post the write, and keep rendering with duplicate reads instead of pausing the UI to "refresh". It also treats shopping output as a first-class derived surface (counts, normalized lines, preview text, editor health), so the app is always ready to show *something* coherent even while the network is catching up.

Read the guide

If you tried mutation-without-invalidation, what did you render as evidence that the UI is "ahead" of the server? Do you store derived shopping lines as a single normalized structure, or keep multiple derived views (per recipe vs bundle)? How do you keep the editor from drifting when the detail route does fresh reads while you're mid-edit? Did you use draftKey remounts for re-baselining, or did you solve it with explicit reset actions?

Comments (20)

Back to latest
Amina Farouk
Jan 12, 2023 · 10:40 PM

We adopted the "keep moving" posture and it made the app feel faster even when the network wasn't.

The key was admitting (in the UI) when we were optimistic.

Keira Santos
Jan 18, 2023 · 06:14 AM

Concrete evidence: we rendered data-mutation-lane="optimistic|confirmed|reconciling" on the route shell.

It sounds silly, but it instantly explained screenshots.

Dmitri Kline
Jan 27, 2023 · 03:33 PM

Normalized shopping lines were worth storing because three panels cared about the same thing (list, preview text, "copy to clipboard"). We kept one canonical derived shape and derived presentation from it:

ts
type Line = { key: string; label: string; qty: number; unit: string };

function deriveShoppingLines(doc: any) {
  const recipe = doc.read('draft');
  const lines = normalizeIngredients(recipe.ingredients);
  doc.write('shoppingLines', lines);
  doc.write('shoppingText', lines.map((l: Line) => `${l.qty} ${l.unit} ${l.label}`).join('\n'));
}

If the text is stored, the UI can show it instantly and it doesn't drift between panels.

Marisol Vega
Feb 05, 2023 · 06:22 PM

Counterpoint: duplicate reads can get you into "last write wins" surprises.

We had a bug where the optimistic doc wrote a title, then a fresh read re-applied an older title, then the mutation confirmed and flipped again.

Rowan Pierce
Feb 09, 2023 · 03:19 AM

Yep, reconciliation is the hard part. We ended up storing an optimisticRevision and rendering it as evidence, then only applying server reads if they were >= that revision.

Not perfect, but it stopped the flicker and made the "why did it change" question answerable.

Salma Qureshi
Feb 18, 2023 · 10:55 AM

We used draftKey remounting for one thing only: "start a new recipe based on this recipe".

For everything else we did explicit resets so we could keep field focus stable.

Noah Vance
Mar 04, 2023 · 02:05 AM

The guide's editor health score is underrated.

Even if it's a fake number, it forces you to define what "healthy" means (no empty ingredient lines, step count nonzero, title present, etc.).

Jasper Nguyen
Mar 19, 2023 · 05:44 PM

We implemented the health score as a derived key and used it to gate mutation attempts. It made validation feel less like random red errors and more like a continuous signal.

ts
function deriveEditorHealth(doc: any) {
  const d = doc.read('draft');
  let score = 100;
  if (!d.title.trim()) score -= 30;
  if (d.ingredients.filter((x: string) => x.trim()).length === 0) score -= 30;
  if (d.steps.filter((x: string) => x.trim()).length === 0) score -= 20;
  doc.write('editorHealth', Math.max(0, score));
  doc.write('isValid', score >= 60);
}
Grace McCoy
Apr 02, 2023 · 07:21 AM

I struggled with the "mutation without invalidation" phrasing until I read it as: don't pause the UI just to satisfy a cache ritual.

It's closer to: "render what you know, then let reads converge".

Hana Saito
Apr 18, 2023 · 12:00 PM

We kept per-recipe derived shopping output and a separate bundle derived output.

The trick was making the bundle explicitly its own derived key so you don't accidentally mix them.

Inez Park
May 06, 2023 · 09:38 AM

For drift: we separated the server read (recipe) from the editor draft (draft) and never overwrote draft from reads unless the user explicitly "rebase"d.

Otherwise duplicate reads are constantly fighting the editor.

Benji Rios
May 23, 2023 · 01:15 AM

We also rendered an evidence line like: serverRev=12 optimisticRev=14.

People complained at first, then they stopped filing impossible bugs.

Tomas Rivera
Jun 12, 2023 · 07:44 PM

The guide keeps mentioning "duplicate reads" and it maps to the docs posture pretty well.

Related: Data Fetching and Caching (Best Practices) and Testing and Debugging.

Priya Liao
Jul 04, 2023 · 05:02 AM

Short comment: storing preview is the only way I can get consistent UX in editors.

If preview is derived live in render, it ends up slightly different across panels, which reads as "bug" to users.

Evan Brooks
Jul 21, 2023 · 11:10 AM

Has anyone actually shipped "no invalidation" with offline mode?

I feel like offline makes the optimistic lane the primary lane and reconciliation becomes permanent.

Camille Duarte
Jul 25, 2023 · 08:09 PM

We did, but we treated reconciliation as a first-class UI story (queued writes, conflict banner, "fork a copy" action).

The guide posture still works, you just have to admit that "confirmed" might be delayed for hours.

Sofia Valdez
Aug 19, 2023 · 08:45 AM

The best part for me was that the shopping list view isn't a separate app; it's a derived view of the same document truth.

Once you store normalized lines, everything else is just rendering from that contract.

Lena Fischer
Sep 15, 2023 · 04:05 PM

Counterpoint to storing lots of derived stuff: you can also store just shoppingLines and compute text from it.

We didn't store shoppingText because it was easy to derive and we never had sibling drift there.

Dmitri Kline
Sep 18, 2023 · 09:22 AM

That seems reasonable. We stored text only because we had three separate "copy" surfaces and they were diverging.

If you only have one, live derivation is probably fine.

Mina Calder
Oct 11, 2023 · 02:31 AM

One implementation note: do not let ingredient parsing happen in an effect after render.

If the derived keys exist to make render correct, derive them in the mutation path or in a render-visible helper.

Nina Kapoor
Nov 06, 2023 · 11:58 PM

I wish the guide emphasized the "single call site" idea more: mutate, then derive, then render from derived keys.

That's what kept us from forgetting to update ingredientCount and stepCount in random places.

Talia Moss
Dec 02, 2023 · 01:13 PM

The guide reads like an argument that caches should be a preference, not an authority. I'm into it.

Ari Novak
Jan 07, 2024 · 04:55 AM

Late to the thread, but the posture worked best when we treated "freshness" as a product decision.

Sometimes being consistent is more valuable than being newest.