Sharing state between components - discussion (2023-02-04)
The "Sharing state between components" page is the canonical "lift it up" story, but real apps end up with more nuance: routes, panels, drafts, and derived outputs that multiple components must agree on. In the React Tips vocabulary, I like thinking in terms of explicit contracts: where does truth live, and what evidence do we render when components disagree?
When you share state across panels/routes, do you still lift to a parent component or do you use a route document/vault? How do you keep shared state from becoming a global junk drawer? Do you store derived "shared outputs" (counts, selections) so UIs stay consistent, or compute them live?
Comments (18)
Back to latestWe still lift to a parent when the relationship is local and the parent is stable.
But for route-level sharing, we use a route doc so back/forward replays it and the contract is explicit.
Long-form take: the "lift up" story breaks down when there isn't a single natural parent (e.g. list + detail + overlay). In those cases, a route doc is basically the "parent"—a stable place where state lives and can be observed. We log shared-state transitions so disagreements are visible:
txt
[tips] sharedState=selection set=doc_19 reason=list:click
[tips] sharedState=selection set=null reason=overlay:closeCounterpoint: route docs/vaults are easy to abuse. Everything becomes shared state because it's convenient.
We require a clear reason for shared state: either navigation, cross-panel coherence, or persistence across remount.
Yes. Shared state should be a contract, not a convenience.
If it isn't part of the user's experience, it probably shouldn't be shared at all.
We store derived shared outputs when they are used in multiple places (badge counts, selection summaries).
Otherwise components compute them slightly differently and the UI feels inconsistent.
For "global junk drawer", we split state by domain documents rather than one mega store.
Selection doc, filter doc, composer doc. Each has a small vocabulary and evidence keys.
One trick: give shared state an identity boundary and log it when it changes. It prevents mysterious cross-panel resets:
txt
[tips] identity=project:42 selection cleared reason=identity:changeLong-form argument: shared state should be "replayable".
If back/forward can't replay it, it isn't really part of the route experience. It's hidden state.
Route docs solve that by being explicit about what gets replayed.
Counterpoint: lifting to a parent is still the best when it's possible. It's simple and it stays local.
I only reach for docs when multiple routes/panels truly need to agree.
We rendered a "shared contract" evidence line in debug mode so disagreements were obvious:
txt
shared: selection=doc_19 filter=status:open panel=detailDocs tie-in: the state management and patterns pages are the best references for contract-driven shared state.
Related: State Management (Best Practices) and Patterns Library.
We treated drafts as multi-truth: local draft in component, committed truth in route doc on blur/submit.
Sharing drafts globally made typing laggy and made undo/redo weird.
Counterpoint: docs/vaults can make testing harder if you don't expose evidence in the UI.
If the UI renders lane and selection, tests stay simple. If not, tests reach into internals and become brittle.
We also used a merge rule when multiple panels write selection at once (newer wins, but log conflicts). Conflicts shouldn't be silent:
txt
[tips] sharedState conflict key=selection winner=panel:list loser=panel:overlayThe page's mental model is still correct: pick one place for truth.
In real apps, the trick is choosing a place that matches navigation and user intent boundaries.
Long-form counterpoint: some shared state should not be shared at all; it should be derived independently from a stable input.
If you share a derived count and it becomes stale, you get trust issues. Sometimes recomputing is safer than caching.
The repo's emphasis on evidence is what makes shared state survivable.
If you share state and you can't see it, debugging becomes folklore.
We started small: share selection only. Everything else stayed local until we had a clear need.
That prevented the store from turning into a dumping ground.
If you're implementing shared state, write down the contract first: keys, identity boundaries, and evidence lines.
Then code it. Otherwise shared state grows by accident.