[Guide] Build an Issue Tracker - implementation notes
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.
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 latestThe "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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Agree. "Allowed to disagree" doesn't mean "allowed to be undefined".
We wrote an ownership table in docs: key -> owner -> reconcile rule.
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.
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.
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.
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.
We also rendered a tiny evidence line like uiRev=12 dataRev=10.
It made "why did my edit revert" questions answerable.
Has anyone used this posture with real-time updates?
It feels like competing stores becomes the *default* state under websockets.
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.
That helps. I was trying to keep UI and data in lockstep and it was a mess.
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.
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.
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.
The key takeaway for me: disagreement is inevitable; hiding it is optional.