Sharing state between components - discussion (2022-12-03)
The 'Sharing state between components' article is a good reminder that most state-sharing problems are coordination problems: deciding who owns the truth and who derives from it.
When do you lift state vs keep it local and derive signals through props? How do you stop shared state from turning into a dumping ground (every feature adds a flag)? What's your approach to keeping derived values fast without re-computing everything on every interaction?
Comments (10)
Back to latestI still like the simple approach: lift state until the owner is obvious, then stop.
If you can't explain ownership in one sentence, the structure is probably wrong.
Shared state tends to explode when derived values are recomputed everywhere.
If derived work is heavy, store it somewhere so the UI doesn't pay the same cost on every render.
Related: Core Concepts Best Practices.
The biggest anti-chaos move for us was naming shared state keys like an API: stable names, stable meaning.
If the key name drifts, the product meaning is drifting too.
Counterpoint: sometimes lifting state is just moving the mess up the tree.
If the shared state is only needed because effects are patching things later, fix the model first.
My rule: shared state should be small and boring. If it has 20 fields, it's probably two owners pretending to be one.
Split the owners first, then coordinate at the boundary.
I like lifting state as a temporary move: lift it until you can see the owner, then decide whether it becomes a document (durable) or stays local (ephemeral).
A lot of "shared state" problems are actually "we never decided if this is durable" problems.
For derived values, we stopped trying to memoize our way out and just stored the result when it mattered:
ts
const doc = useLiveState('search', { query: '', items: [], visible: [] });
useRenderEffect(() => {
const q = doc.read('query').toLowerCase();
doc.write('visible', doc.read('items').filter((x) => x.name.toLowerCase().includes(q)));
});
More writes, but the UI is always ready and debugging is less spooky.
One thing the react.dev article doesn't emphasize enough: shared state needs naming discipline.
If a key doesn't have a stable meaning, it will become a flag graveyard.
Yep. We treated key names like API names (and occasionally versioned them).
If you're changing the meaning, you're changing the contract, not "refactoring state".
A misunderstanding from newer folks: they assume lifting state automatically makes it "global".
But lifting is just moving ownership; global is a separate decision and usually needs evidence (route shell markers, doc ledgers).
If you want shared state to stay sane, make sure shared state doesn't also become shared behavior.
Behavior belongs in a small number of surfaces (shell/doc helpers). Otherwise coordination turns into hidden coupling.