skip to content

State Modeling

Deciding what actually belongs in state and where it should live, before reaching for any sharing mechanism. Interviewers use these questions to see whether you keep state minimal and close to its consumers, or scatter redundant copies that quietly drift out of sync.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

12

In React, two sibling components need the same value — one displays it and the other edits it. Where does that state go, and what does each sibling receive instead?

level: juniorimportance: must knowfreq 78%

answer

  1. siblings cannot talk sideways
  2. nearest common ancestor
  3. value down, events up
  4. child gives up its own copy
  5. one useState behind both renders

basics

~20 s

Move the state into the siblings' nearest common ancestor. That parent owns it with useState and passes the current value down to both children, plus a callback that lets the editing child ask for a change.

solid answer

~50 s

State that two siblings both depend on cannot live in either of them — a component can only pass data to what it renders, so siblings have no way to reach each other. I lift the state to their nearest common ancestor: that parent holds it with `useState` and passes two things down — the current value to whichever children display it, and a callback such as `onChange` to the child that needs to change it. The editing child stops keeping its own copy and becomes controlled for that value: it renders what it is handed and reports intent upward. That gives a single source of truth, so the two children can never disagree, and it keeps React's flow one-directional — data down through props, events up through callbacks. I lift only as far as the closest ancestor both children sit under.

code

javascript · 25 lines
javascript
import { useState } from 'react';

function SearchBox({ query, onQueryChange }) {
  return (
    <input
      value={query}
      onChange={(e) => onQueryChange(e.target.value)}
      placeholder="Search"
    />
  );
}

function ResultCount({ query }) {
  return <p>{query ? `Searching for "${query}"` : 'Showing everything'}</p>;
}

export default function Filters() {
  const [query, setQuery] = useState('');
  return (
    <>
      <SearchBox query={query} onQueryChange={setQuery} />
      <ResultCount query={query} />
    </>
  );
}

go deeper

for a junior

Be ready to name the move in one sentence: shared state goes to the nearest common parent, which passes the value down and a change callback back up. Say plainly that siblings cannot read each other's state.

for a middle

Explain the mechanics rather than the slogan: the child stops holding its own copy and becomes controlled for that value, so exactly one useState sits behind both renders and they update in the same commit. Be able to write the wiring.

for a senior

Show judgment about the callback contract — a narrow intent callback instead of a raw setter — and about how far to lift. Point out that repeated lifting of the same value is usually telling you the component boundary is drawn in the wrong place.

for a principal

Own the tradeoff that lifting is coupling. Talk about where ownership lines are drawn so shared state does not creep toward the root, and about keeping a consistent value-plus-onChange convention across the codebase so ownership can move later without rewriting call sites.

## Why the state cannot stay where it is React's data flow is one-directional. A component can hand values to the components it renders, but it has no handle on its parent and no handle on its siblings. If an `Editor` keeps the value in its own `useState` and a `Display` rendered beside it needs the same value, there is no supported way for `Display` to read it — and if both call `useState` independently, they hold two unrelated values that drift apart the instant either one updates. "Lifting state up" is the fix: delete the `useState` from the child and move it into a component that renders both of them. ## The rule: nearest common ancestor The home for a piece of state is the **lowest component that renders every component which reads or writes it**. For one consumer that is the consumer itself (colocation). For two siblings it is their immediate parent. For two components in different branches it is wherever those branches meet. "Nearest" matters as much as "common". Any component above that point is now carrying state it does not use, and every component between it and the consumers has to pass the props through. ## Data down, intent up After lifting, the parent owns the value and hands each child exactly what it needs: ```jsx function Filters() { const [query, setQuery] = useState(''); return ( <> <SearchBox query={query} onQueryChange={setQuery} /> <ResultCount query={query} /> </> ); } ``` `SearchBox` no longer decides anything. It renders `props.query` and, when the user types, calls `props.onQueryChange(next)`. It has become *controlled* for that value: the parent is the single source of truth, and the child is a pure function of its props plus a reporter of user intent. `ResultCount` only reads, so it gets the value alone. This is why React interviews describe the pattern as "data flows down, events flow up". The value makes one trip down the tree; the request to change it makes one trip up; the parent re-renders and both children see the new value in the same commit, so they cannot be out of sync. ## The callback's shape is a design decision Passing the setter itself (`onQueryChange={setQuery}`) is fine when the child genuinely owns the whole value and the parent stores it as-is. Prefer a narrow, named callback when the parent must do something with the change — validate it, normalise it, update two fields at once, or store the value in a different shape: ```jsx <RowList rows={rows} onSelect={(id) => setSelection({ id, at: Date.now() })} /> ``` The narrow callback keeps the parent's state shape private. If the child receives `setState` directly, the child's contract silently depends on how the parent stores things, and refactoring the parent's state breaks the child. ## Do not keep a second copy in the child The classic mistake after lifting is leaving `const [value, setValue] = useState(props.value)` behind in the child "so it stays fast" or "so it works standalone". Now there are two sources of truth: the parent's value and the child's snapshot of it, taken on the first render. Later parent updates do not reach the child, and the UI shows a stale value that no amount of prop passing fixes. If the child needs the value, it reads the prop. ## Lift the minimum, not everything Lifting is coupling: the parent now knows about something two of its children share, and both children now depend on the parent to supply it. So lift the *smallest* thing that must be shared, and only that. A modal's `isOpen` shared by a trigger and the modal itself gets lifted; the modal's internal hover state does not. Purely local state — whether a tooltip is showing, whether a row is expanded and nobody else cares — stays where it is. When the same value gets lifted twice more and starts travelling through several layers that ignore it, that is a signal to reconsider the structure, not automatically a signal to reach for a global store. ## What interviewers are listening for Three things. First, that you say *nearest* common ancestor rather than "put it in App". Second, that you describe both directions of the wiring — value down, callback up — instead of only the value. Third, that you recognise the single-source-of-truth consequence: the child gives up its own copy, and that is the point of the exercise, not a side effect.

  • Should the child receive the state setter itself, or a narrower callback?
    Pass the setter only when the child owns the whole value and the parent stores it unchanged. Otherwise pass a named intent callback — `onSelect(id)`, `onIncrement()` — so the parent can validate, normalise, or update several fields at once. A narrow callback keeps the parent's state shape private; handing over `setState` makes the child depend on that shape and turns every parent refactor into a child change.
  • After lifting, is the child allowed to keep any state of its own?
    Yes — just not a copy of the lifted value. State nobody else observes stays local: whether a tooltip is visible, whether a row is expanded, an in-progress draft that only becomes shared on submit. Keeping a second copy of the lifted value is the problem, because it is seeded once and then silently diverges from the parent's.
  • How do you decide you have lifted far enough?
    Stop at the lowest component that renders every reader and writer. If you have to go higher, that is the correct home; if you went higher than necessary, the extra ancestors carry state they never use and the components in between pass props they ignore. "Put it in App" is over-lifting unless App genuinely is the nearest common ancestor.

saying these in an interview costs you the question

  • Says one sibling can read the other's state directly
  • Reaches for Context or Redux to share between two siblings
  • Keeps a copy in the child and syncs it with an effect
  • Thinks lifting always means moving state to App
  • Passes only the value down and forgets the callback

context

open as a page

A React component holds `firstName` and `lastName` in useState and keeps a third `fullName` state in sync with a useEffect that calls setFullName(firstName + ' ' + lastName). What is wrong with this, and what would you write instead?

level: juniorimportance: must knowfreq 72%

basics

~20 s

fullName is derivable, so storing it duplicates state. The effect runs after React commits, so the screen briefly shows a stale name and every keystroke costs a second render pass. Compute it during render as a plain const.

open as a page

A React component is written as `function Price({ amount }) { const [value, setValue] = useState(amount); ... }` and keeps showing the original number after the parent re-renders with a new `amount`. Why does the useState argument stop having any effect, and what are your options?

level: middleimportance: must knowfreq 78%

basics

~20 s

The argument to useState is only the initial value: React reads it on the component instance's first render and ignores it afterwards, so the copy freezes. Either derive from the prop directly, or treat the copy as genuinely separate state with an explicit reset.

open as a page

In React, a detail form keeps its edits in useState and stays mounted while the user switches from one record to another, so it still shows the previous record's values. Compare fixing this by giving the form a key derived from the record id, by adding a useEffect that resets the state when the id prop changes, and by calling an explicit reset action — which do you choose and why?

level: middleimportance: must knowfreq 62%

basics

~20 s

Prefer the key: rendering the form with key set to the record id makes React unmount the old instance and mount a fresh one, so every piece of state inside resets at exactly the right moment, with no reset code to keep in sync.

open as a page

A React screen keeps every piece of state in the top-level page component, including a search string used only by one deeply nested table. What does that placement cost, and what rule decides where a piece of state actually belongs?

level: middleimportance: should knowfreq 56%

basics

~20 s

State belongs in the lowest component that needs it. Hoisting a table's search string to the page re-renders the whole screen on every keystroke, piles unrelated concerns into one component, and stops the table from being usable on its own.

open as a page

You are building a reusable React Tabs component. How do you let it manage the selected tab itself by default while still allowing a parent to own that state, and what does the component do differently in each mode?

level: middleimportance: should knowfreq 46%

basics

~20 s

Support both modes: seed internal state from a defaultValue prop, and treat the component as controlled whenever a value prop is supplied. Controlled, it renders the parent's value and only reports changes; uncontrolled, it updates its own state.

open as a page

A React table receives a `rows` prop and tracks the highlighted row with `const [selectedRow, setSelectedRow] = useState(null)` holding the whole row object. Why is storing the row's id usually the better model, and what breaks in the object version?

level: middleimportance: should knowfreq 54%

basics

~20 s

Storing the object duplicates data the rows prop already owns, so the stored copy goes stale when rows are refetched or edited. Store the id — the minimal fact React cannot compute — and look the row up during render.

open as a page

A React comment editor is rendered as <CommentBox key={draftText} authorId={authorId} /> so that it clears when the author changes, and now the textarea loses focus after every keystroke. What is wrong with that choice of key value, and what should it be instead?

level: middleimportance: should knowfreq 38%

basics

~20 s

The key is derived from content that changes while the component is alive, so every keystroke is a new identity and React remounts the editor, destroying the focused element. Key on the stable identity being edited, such as the author or comment id.

open as a page

After lifting selection state into a page component, the value and its onChange are threaded through four intermediate React components that never use them. How do you judge whether that is a real problem, and what would you change?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Judge it by how many layers ignore the props and how often the contract changes; one or two hops are fine. The first fix is composition — let the owner build the elements and pass them as children or slot props, so the middle layers never see the data. Context comes after that.

open as a page

A teammate argues that filtering and sorting a 5,000-item list on every React render is wasteful and wants to keep the sorted result in useState, updated by an effect whenever the inputs change. How do you respond, and when is useMemo the right tool instead?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Storing the result adds a second source of truth, a stale frame and an extra render on top of the same computation, so it is strictly worse. Measure first; if the derivation is genuinely hot, use useMemo, which caches within the render and keeps one source of truth.

open as a page

A React team fixed a stale detail panel by putting key={selectedId} on their top-level page component, and now switching records also collapses the sidebar, jumps the scroll to the top and re-initialises an expensive chart. How do you decide which subtree the key actually belongs on?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Put the key on the smallest component that owns exactly the state that should die with the identity change. If that state is mixed with state that must survive, extract the identity-scoped part into its own component and key that one.

open as a page

In a React master-detail editor, remounting the detail pane with key={recordId} correctly clears it but also throws away unsaved edits when the user clicks another record and comes back. How do you decide between identity-keyed remount and keeping per-record draft state, and what does each choice commit you to?

level: principalimportance: should knowfreq 30%

basics

~20 s

Decide by whether losing the state is the intended product behaviour. If edits are disposable, keyed remount is the simplest correct answer. If they are work the user expects back, the draft must live above the keyed boundary, keyed by record id, and you own its lifetime.

open as a page