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

[Guide] Build an Issue Tracker - implementation notes

Started by Lena Fischer
guideissuescompeting storescreateStateVaultuseRenderEffectuseFlowEffectrender-visible reconciliationderived queues

The Issue Tracker guide is the most explicit so far about the "competing stores" baseline: a data vault holds canonical issues/comments, a UI vault holds selection/filters/drafts, and they're allowed to disagree. The route renders that disagreement and then reconciles it with render-visible effects so the merge rule is readable and testable.

Read the guide

How do you decide what belongs in the UI vault vs the data vault (especially for drafts and optimistic writes)? Did you actually store derived keys like visibleIssueIds and detailSummary, or did you derive them live in render? What did you render as evidence when the UI and data vault disagreed (revision keys, last intent, merge lane)? Where did you put the reconciliation logic so it stayed visible without turning the shell into a giant controller?

Comments (20)

Back to latest
Marisol Vega
Jan 17, 2023 · 03:55 AM

The "allowed to disagree" framing made a bunch of our existing bugs make sense.

We were pretending selection state and server state were the same thing, and they just aren't.

Dmitri Kline
Jan 24, 2023 · 10:18 AM

UI vault vs data vault rule of thumb: if it's about *what the user is doing*, UI vault.

If it's about *what exists in the system*, data vault.

Keira Santos
Feb 06, 2023 · 07:30 PM

We stored visibleIssueIds and groupsByStatus and it paid off immediately.

Without stored derivations, the list panel and the detail panel disagreed about "what is open" under fast updates.

Camille Duarte
Feb 21, 2023 · 06:22 AM

Counterpoint: storing derived queues can be a consistency trap if you don't centralize mutations.

We had a path that edited an issue title but forgot to re-derive the search matches, and the UI looked haunted.

Lena Fischer
Feb 24, 2023 · 05:10 PM

Yep. The only way it worked for us was making reconciliation/derive a first-class step that runs after any write.

If derivations are optional, they get skipped.

Grace McCoy
Mar 10, 2023 · 12:18 PM

Evidence: we rendered data-selected, data-filter-status, and a data-merge-lane string on the shell.

Once you can see the lane, arguments about "React rerender bugs" got a lot quieter.

Jasper Nguyen
Mar 28, 2023 · 02:40 AM

We kept reconcile logic in a dedicated IssuesReconciler component like the guide suggests.

It felt weird at first, but it kept the rule visible without bloating list/detail components.

Nina Kapoor
Apr 15, 2023 · 09:05 AM

We used a single reconcile function that writes derived keys for both vaults:

ts
function reconcile(ui: any, data: any) {
  const selectedId = ui.read('selectedId');
  const issue = selectedId ? data.read('issuesById')?.[selectedId] : null;
  ui.write('selectedIssueExists', Boolean(issue));
  ui.write('detailSummary', issue ? `${issue.title} (#${issue.id})` : '');
  ui.write('mergeLane', issue ? 'ok' : 'no-selection');
}

It's blunt, but it made the app's "truth merge" something you can actually inspect.

Owen Price
May 03, 2023 · 08:50 PM

Drafts: we kept drafts in the UI vault and never overwrote them from data reads unless the user hit a rebase action.

Otherwise background reads fight the editor.

Salma Qureshi
May 28, 2023 · 01:12 PM

I liked the guide's "route renders disagreement" line because it's honest.

So many apps hide disagreement in effects, which makes it look like random flicker instead of a rule.

Benji Rios
Jun 19, 2023 · 06:37 AM

Counterpoint: competing stores can be an excuse for not deciding ownership.

We had to be strict about which store owns which keys, otherwise everything duplicated everywhere.

Amina Farouk
Jun 23, 2023 · 06:29 PM

Agree. "Allowed to disagree" doesn't mean "allowed to be undefined".

We wrote an ownership table in docs: key -> owner -> reconcile rule.

Theo Bennett
Jul 18, 2023 · 11:01 AM

The flow effect approach is the part that feels most like a "standard React" claim in this repo.

It forces merge logic to be readable in code review instead of hiding it in a handler.

Priya Liao
Aug 10, 2023 · 04:20 AM

We used a derived triageLabels array and it made list scanning much faster.

Even if the labels are imperfect, being consistent matters more than being correct.

Lina Ortega
Sep 02, 2023 · 09:15 PM

Docs tie-in: this guide reads like a concrete application of the state docs' "store derived state" posture.

Related: State Management (Best Practices) and Testing and Debugging.

Hana Saito
Oct 11, 2023 · 12:48 PM

One bug class this eliminated: stale selection when filters change.

If reconcile sees selectedId isn't in visible ids anymore, it writes the selection away and the UI self-corrects.

Sofia Valdez
Nov 28, 2023 · 03:03 AM

We also rendered a tiny evidence line like uiRev=12 dataRev=10.

It made "why did my edit revert" questions answerable.

Evan Brooks
Feb 02, 2024 · 06:40 PM

Has anyone used this posture with real-time updates?

It feels like competing stores becomes the *default* state under websockets.

Camille Duarte
Feb 06, 2024 · 10:20 AM

We did. The trick was treating realtime as just another writer to the data vault, then reconcile makes it visible.

The UI vault doesn't need to pretend it's realtime; it just needs to be coherent.

Evan Brooks
Feb 08, 2024 · 07:05 PM

That helps. I was trying to keep UI and data in lockstep and it was a mess.

Tomas Rivera
May 22, 2024 · 07:33 AM

We used render-fetch + bypass posture like the guide suggests and it kept the app feeling fresh.

But we still stored derived keys so freshness didn't mean churn.

Ari Novak
Oct 14, 2024 · 04:58 PM

If you try this and it feels wrong, start by splitting just one thing: selection vs data.

Once that split exists, the rest of the guide reads much more naturally.

Inez Park
Apr 06, 2025 · 05:44 AM

We added a "reconcile trace" array for debug builds and it was surprisingly useful.

It listed the rule that last ran, so you could see exactly what changed state.

Jules Kwon
Nov 18, 2025 · 08:12 PM

The key takeaway for me: disagreement is inevitable; hiding it is optional.