skip to content

State Ownership & Lifting

Choosing which instance owns a value: local state, lifting to the lowest common ancestor, stored versus derived, one status field instead of booleans that contradict. A favorite opening question.

on this pageshow

questions

5

In a component framework, where should a value live when two sibling components both need to read and change it?

level: juniorimportance: must knowfreq 82%

answer

  1. one value, exactly one owner
  2. ask who reads it
  3. lowest common ancestor, not the root
  4. value down, change request up
  5. two copies always drift

basics

~20 s

The 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 s

Keep 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

A component copies an incoming input value into its own state when created; what breaks when the parent later passes a different value?

level: middleimportance: must knowfreq 66%

basics

~20 s

Nothing updates the copy. An initial value is read once when the state is created, so the component keeps rendering the old snapshot while the parent holds the new value: two truths for one concept.

open as a page

Why is tracking one operation with three independent boolean flags in component state bug-prone, and what shape replaces it?

level: middleimportance: should knowfreq 58%

basics

~20 s

Three independent booleans describe eight combinations when the operation has about four real states, so contradictory ones are reachable and each write must set several flags. One status field with a closed set of named values makes the illegal combinations unrepresentable.

open as a page

A value owned near the root is threaded through five components that never read it, and every keystroke re-renders most of the page; how do you decide whether the owner sits too high?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Decide from the readers, not the depth. List every component that reads or writes the value, take the lowest one containing them all, and compare it with the current owner; the levels between that only pass it along are the cost.

open as a page

A detail panel keeps the previous item's unsaved edits after the user selects a different item; who should own that state?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The panel owns draft state that outlives its subject. Either stop storing it and derive the fields from the selected item, give the panel a new identity per item so the old state is discarded, or reset deliberately on selection change.

open as a page