Sharing state between components - discussion (2022-08-27)
We keep revisiting the same decision in reviews: a value needs to be shared across siblings, but we don't want to turn everything into a global store.
When do you lift state vs introduce context vs push it into route state because back/forward should reproduce it? If you keep a vault/store, what guardrails keep it from becoming a dumping ground? How do you make shared state explainable when it starts affecting navigation and caching posture?
Comments (10)
Back to latestIf the URL should reproduce it, don't fight it. Make it route truth.
We use a simple rule: share via props/context until the behavior becomes a contract (deep links, back/forward, support triage).
Then we promote it to route state and store derived evidence once so panels don't drift.
Related: Routing and Navigation and Core Concepts Best Practices.
We keep context from becoming magical by using a typed surface and rendering a tiny signal in the shell:
tsx
type FiltersSurface = { q: string; setQ: (q: string) => void };
function FiltersSignal({ q }: { q: string }) {
return <div data-signal="filters" data-value={JSON.stringify({ q })} />;
}Counterpoint: pushing everything into route state can make simple components harder to reuse.
We keep local state for truly local knobs and only promote the ones users expect to share.
Agree. The promotion trigger for us is not shared but reproducible.
If support needs to explain it from a screenshot or URL, it belongs in the contract.
Yep. Reproducibility is a better rule than shared.
Vaults are fine, but only when the keys are owned. A vault with ad hoc keys is just hidden coupling with nicer ergonomics.
We added a guardrail: no vault write without a reason string.
It sounds goofy, but it stopped people from writing values that became accidental dependencies later.
We also store derived evidence once during sharing refactors. If two panels both need filteredIds, write it to the doc and have both panels read it, instead of each panel deriving and disagreeing.
tsx
const doc = useRouteDoc('search');
const ids = useMemo(() => filterIds(items, q), [items, q]);
doc.write('filteredIds', ids);
return <div data-signal="filters" data-count={ids.length} />;The best shared state is the one you can delete later. Keep the surface small so migrations don't become archaeology.
If shared state changes navigation and you don't render a signal, you're going to get redirect bugs that read like ghost stories.
The boring approach scales: props -> context -> route truth. Don't skip steps unless you have a real contract reason.