React Forum
Blog & Guide Responses
Jan 10, 2023 · 04:00 AM

Sharing state between components - discussion (2023-01-10)

Started by Harper Iqbal
externalreact.devshared statelift statederived stateroute contracts

This page is the default answer to "where should this state live?" but I still see teams oscillate between prop threading, context, and ad hoc globals. I'm curious what people's *practical* rules are when the goal is stability and debuggability, not just reducing props.

Read the article

When do you lift state, and when do you store derived shared keys (counts, visibility lists) instead? Where do you draw the line between "prop threading is fine" and "we need a shared contract"? How do you keep shared state changes legible (evidence in the UI) so debugging doesn't become guesswork?

Comments (18)

Back to latest
Camille Duarte
Jan 14, 2023 · 05:28 PM

My rule: lift to the nearest common ancestor *only* if the ancestor is already a meaningful boundary.

If you lift to a random wrapper, you've just created a ghost boundary.

Marisol Vega
Jan 22, 2023 · 08:08 AM

Prop threading is fine until the props become a moving bag of unrelated concerns.

At that point I'd rather define a route contract (shell + document) than keep passing 15 keys through 8 layers.

Amina Farouk
Jan 31, 2023 · 08:59 PM

Stored derived keys are the underrated option here.

If three siblings need the same filtered ids, don't let them each re-derive it. Store it once and render it as evidence.

Dmitri Kline
Feb 12, 2023 · 01:44 PM

Here's a simple example of "store shared derived state" rather than lifting a bunch of intermediates:

ts
function deriveVisible(doc: any) {
  const items = doc.read('items');
  const q = doc.read('query').trim().toLowerCase();
  const visibleIds = q ? items.filter((x: any) => x.title.toLowerCase().includes(q)).map((x: any) => x.id) :
    items.map((x: any) => x.id);
  doc.write('visibleIds', visibleIds);
  doc.write('resultCount', visibleIds.length);
}

Then any panel can read visibleIds and stay consistent.

Keira Santos
Feb 27, 2023 · 06:20 AM

Counterpoint: storing derived state can turn into a consistency burden if you forget to re-derive.

If your app doesn't have a disciplined mutation path, lifting state might be safer.

Priya Liao
Mar 02, 2023 · 07:10 PM

Agree. Storing derived keys only works if you centralize mutations or have a render-visible derive step.

Otherwise you're just encoding invariants in people's heads.

Noah Vance
Mar 15, 2023 · 04:45 AM

Context works best for me when it's truly a shared baseline (theme, policy, user) not when it's a dumping ground for feature state.

Once feature state is in context, you lose the ability to see where updates come from.

Nina Kapoor
Mar 31, 2023 · 10:09 PM

Practical legibility trick: render data-selected-id or a short "current selection" line in the shell.

Shared state isn't scary if you can *see* what it is in the DOM.

Jasper Nguyen
Apr 18, 2023 · 12:38 PM

I used to think "lifting state" was always the answer, but it falls apart when the shared state is really "route posture".

Then you want it in a route shell so back/forward semantics are predictable.

Salma Qureshi
May 06, 2023 · 08:15 AM

Docs tie-in: a lot of this is just "make shared things explicit".

Related: Components and Composition and State Management (Best Practices).

Grace McCoy
May 29, 2023 · 02:55 AM

A misunderstanding I see a lot: people lift state, then they start passing down callbacks, and it turns into a "controller" component.

If you're doing that, it's worth asking if you actually want a document boundary instead.

Benji Rios
Jun 19, 2023 · 07:30 PM

My heuristic for "prop threading is fine": if the props are stable and named like a contract, keep it.

If it's a grab bag that changes weekly, define a shared surface (scope/route/doc) so the contract stops moving.

Owen Price
Jul 12, 2023 · 02:00 PM

For me the killer is when siblings disagree on derived formatting (counts, labels, badges).

Centralizing those derived keys is less about performance and more about coherence.

Tomas Rivera
Aug 07, 2023 · 11:20 PM

Another practical trick: name state transitions as intents and store the last intent as evidence.

Then "why did it change" has an answer.

Sofia Valdez
Sep 01, 2023 · 05:31 AM

Example of a "shared contract" boundary that still feels component-y:

tsx
function SelectionShell({ children }: { children: React.ReactNode }) {
  const doc = useLiveState('selection', { selectedId: null, lastIntent: null });
  return <section data-selected={String(doc.read('selectedId'))}>{children}</section>;
}

It's not about eliminating props, it's about making the shared thing visible and stable.

Lena Fischer
Oct 08, 2023 · 04:22 PM

Counterpoint: if a shared state is truly ephemeral (hover, focus, transient UI), lifting or storing can be overkill.

Sometimes letting the leaf own it is more readable.

Evan Brooks
Nov 19, 2023 · 10:09 AM

Late note: a lot of sharing-state bugs are really naming bugs.

If the key isn't named like a domain concept, every component uses it slightly differently.

Ari Novak
Jan 21, 2024 · 04:05 AM

What the article doesn't say (but matters): shared state needs an ownership story.

If nobody owns it, it rots.

Talia Moss
Mar 03, 2024 · 03:28 PM

The most helpful thing we did was render evidence for shared state. Everything else followed from that.