Lifting State Up

Difficulty: Intermediate

In React, data flows in one direction - from parent to child via props. When two sibling components need to share or synchronise state, neither sibling can own that state alone. The solution is to 'lift' the state up to their closest common parent and pass it down along with an updater function. This pattern is called Lifting State Up.

The parent becomes the single source of truth for the shared value. Each child receives the current value as a prop and calls the parent-provided callback whenever the value should change. Because React re-renders the parent when its state changes, both children automatically receive the latest value on the next render cycle.

Lifting state is the simplest form of state sharing in React and should be your first choice before reaching for Context or external stores. It keeps data flow explicit and easy to trace. However, when the distance between the state owner and the consumer grows (prop drilling through many layers), you may want to explore other patterns such as Context or Zustand.

A practical example is a temperature converter where Celsius and Fahrenheit inputs must stay in sync. The parent holds one canonical temperature value and each input converts from that value. When either input changes, the parent updates and both inputs reflect the new temperature.

Remember: every piece of state should have exactly one owner. If two components need the same state, find the nearest common ancestor and let it own the data.

Code examples

Sibling components sharing state via parent

function Parent() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <Display count={count} />
      <Controls onIncrement={() => setCount(c => c + 1)} />
    </div>
  );
}

function Display({ count }) {
  return <p>Count: {count}</p>;
}

function Controls({ onIncrement }) {
  return <button onClick={onIncrement}>Add 1</button>;
}

Parent owns the count state. Display reads it, Controls writes it through the callback. Both stay in sync because the parent re-renders on every state change.

Temperature converter - syncing two inputs

function TemperatureCalculator() {
  const [temp, setTemp] = React.useState({ value: '', scale: 'c' });

  const celsius = temp.scale === 'f'
    ? ((parseFloat(temp.value) - 32) * 5 / 9).toFixed(2)
    : temp.value;
  const fahrenheit = temp.scale === 'c'
    ? ((parseFloat(temp.value) * 9 / 5) + 32).toFixed(2)
    : temp.value;

  return (
    <div>
      <TemperatureInput scale="c" value={celsius}
        onChange={v => setTemp({ value: v, scale: 'c' })} />
      <TemperatureInput scale="f" value={fahrenheit}
        onChange={v => setTemp({ value: v, scale: 'f' })} />
    </div>
  );
}

function TemperatureInput({ scale, value, onChange }) {
  const label = scale === 'c' ? 'Celsius' : 'Fahrenheit';
  return (
    <label>
      {label}: <input value={value}
        onChange={e => onChange(e.target.value)} />
    </label>
  );
}

The parent stores one canonical temperature plus which scale was last edited. Both inputs derive their display value from that single source of truth.

When lifting state causes prop drilling

// Deeply nested tree - state lifted to App but consumed far below
function App() {
  const [user, setUser] = React.useState(null);
  return <Layout user={user} setUser={setUser} />;
}

function Layout({ user, setUser }) {
  return <Sidebar user={user} setUser={setUser} />;
}

function Sidebar({ user, setUser }) {
  return <UserMenu user={user} setUser={setUser} />;
}

function UserMenu({ user, setUser }) {
  return user
    ? <span>{user.name}</span>
    : <button onClick={() => setUser({ name: 'Alice' })}>Log in</button>;
}

Every intermediate component (Layout, Sidebar) must forward props it does not use. This is prop drilling and is a signal you may want Context instead.

Key points

Concepts covered

lifting state, single source of truth, sibling communication, prop drilling, inverse data flow