In a component framework, where should a value live when two sibling components both need to read and change it?
answer
- one value, exactly one owner
- ask who reads it
- lowest common ancestor, not the root
- value down, change request up
- two copies always drift
basics
~20 sThe lowest common ancestor of both siblings should own it: that component holds the value, passes the current value down as an input to each sibling, and passes down a function each sibling calls to request a change.
solid answer
~50 sKeep state as local as possible, and lift it only when a second component needs it. Here two siblings read and write one value, so neither can own it — the owner becomes their **lowest common ancestor**. That component declares the state, hands the current value to each sibling as an input, and hands each one a function describing the change it may request. The siblings stop holding that value at all, which is the point: one cell, one writer, one source of truth. The alternative — a copy in each sibling kept in step by watching the other — gives you two cells that disagree the moment one update is missed. Lift to the *lowest* ancestor that contains every reader, not to the root: an owner higher than it needs to be couples unrelated parts of the tree to a value they never use.
go deeper
Recall the rule and the direction of flow: state lives in the lowest component that contains everyone who needs it, the value travels down as an input, and a change travels up as a function call.
Be able to perform the lift step by step and justify the choice of ancestor, and explain why two copies kept in step by watching each other is a drift bug rather than a caching optimisation.
Show that you pick the owner from the actual set of readers and revisit it as code changes, and that you know what a write costs in the reactivity model you are working in.
Frame ownership as an interface decision: where state sits determines which subtrees are reusable and which teams must coordinate, so a house rule on default ownership is worth more than case-by-case cleverness.
## What "owning" a value means In a component-based UI, every piece of changing data has exactly one component that **holds** it. That component declares the state cell, reads it when it renders, and is the only place a write to it happens. Every other component either receives the current value as an **input** (a prop) or receives a **function** it can call to ask the owner for a change. Ownership is not a syntax detail: it decides who may write, how much of the tree is invalidated when a write lands, and whether a subtree can be reused elsewhere without dragging the value along. ## Local by default The narrowest owner that works is the right first guess. If exactly one component reads a value — a menu's open flag, a hover highlight, a scratch field nobody else observes — that component owns it. Nothing above or beside it can then depend on it, so the component stays a self-contained unit you can move, reuse, or delete. ## Lift when a second reader appears As soon as a second component must read the same value, or must change it, local ownership stops working. Two moves are available and only one is safe: - **Duplicate it.** Each sibling keeps its own cell and something watches the other to copy changes across. Now one concept has two homes; every future write must remember to update both, and the day one path forgets, the two views disagree with no error anywhere. - **Lift it.** Move the declaration up to the lowest component that contains every reader and writer — their **lowest common ancestor** — and let the children read it as an input. The mechanics of lifting are mechanical enough to do as a checklist: 1. Delete the state declaration from the child that currently has it. 2. Declare the same state in the common ancestor, with the same initial value. 3. Pass the current value down to every component that renders it. 4. Pass down a function per change the children may request — and name it for the **intent** ("select row", "clear filters") rather than exposing the raw write, so the rule about what a valid change is stays with the owner. 5. Sweep for leftovers: any remaining copy of the value is the bug you were trying to remove. | Arrangement | Source of truth | Who may write | Typical symptom | |---|---|---|---| | Local to one component | The one cell it holds | That component | None, until a second reader appears | | Lifted to the lowest common ancestor | The ancestor's cell | The ancestor, on request from children | Children need inputs and callbacks threaded to them | | Copied into both siblings | Ambiguous — two cells | Both, independently | Views drift apart; "it fixes itself if I click twice" | ## Lowest, not highest It is tempting to park everything at the top so it is always reachable. That trades one problem for two. Components between the owner and the readers receive an input only to hand it on, which is noise in every signature along the path and makes those components harder to reuse. And the owner's position decides how much work a write triggers. Frameworks differ on that last point, and it is worth knowing which kind you are in: a runtime that responds to a write by re-running the owner's component function and diffing its output does work proportional to the owner's subtree, so a high owner writing often is expensive; a runtime with fine-grained tracking re-runs only the expressions that actually read the value, so height costs little in time. In both, the *coupling* cost of a too-high owner is the same. ## What is not a lifting problem Three things look like state and are not, and lifting them is how codebases acquire duplicated truth: - **Derived values.** If a value can be computed from state you already own, compute it where it is needed instead of storing and lifting a second cell that can fall behind. - **Values the user should be able to share or reload into.** A current tab, a filter, a page number often belong in the address rather than in a component at all. - **Data owned by a server.** A fetched list is a cached copy of someone else's truth, with its own staleness rules; treating it as ordinary lifted state is what produces two components fetching the same thing. The habit to build is to ask, for every new cell: who reads this? If the honest answer is "one component", keep it there. If it is "these two", the answer is already the lowest thing that contains them both.
- Should the owner pass its raw write function down, or named functions per change?Prefer named functions describing the change — "select row", "increase quantity". The owner then keeps the rules about what a valid change is (clamping, clearing a dependent field, rejecting a duplicate) in one place. Handing out the raw write makes every child a co-author of those rules, and the invariant ends up enforced in some callers and not others.
- What should make you move a lifted value back down into a child?When the set of readers shrinks to one. Refactors routinely remove the second reader that justified lifting, leaving an ancestor holding state only one descendant uses, plus a trail of pass-through inputs. Moving it back down shortens those signatures and makes the child self-contained again — the same test as before, applied in reverse.
- Does lifting a value make the application slower?It depends on the reactivity model. Where a write re-runs the owning component and diffs its subtree, moving the owner up widens the work per write, so an often-changing value lifted high is a real cost. Where reads are tracked individually, only the expressions that read the value re-run and height barely matters. The coupling cost exists in both.
Two roommates who each keep their own shopping list both buy milk, and neither list is the real one. A single list on the fridge that both write to is the lifted version — there is nothing left for the two lists to disagree about.
saying these in an interview costs you the question
- Keeps a copy in each sibling and synchronises them with an effect
- Puts every value in the root component so it is always reachable
- Believes children must never change state an ancestor owns
- Lifts the value but leaves the old local copy in the child
- Thinks sharing a value between two components requires a global store
- Cannot say which component is the single writer of a given value