Lifting State Up

Finding the right owner for a piece of state when two components need it.

What you'll learn

  • Why two components cannot share their own state
  • Moving state to the closest common parent
  • Passing a value down and a callback up
  • How far to lift, and when not to

The problem

State inside a component belongs to that component. Two siblings each calling useState get two separate values, and neither can see the other's.

Below, each panel keeps its own count. They will not agree, no matter what you click:

import { useState } from "react";

function Panel({ label }) {
  const [count, setCount] = useState(0);

  return (
    <div style={{ border: "1px solid #ddd", padding: 12, borderRadius: 6 }}>
      <p style={{ margin: "0 0 8px" }}>{label}: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

export default function App() {
  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, display: "flex", gap: 8 }}>
      <Panel label="Left" />
      <Panel label="Right" />
    </div>
  );
}

That independence is often exactly what you want. But when the two must show the same number, no amount of work inside Panel will do it.

Lift it to the common parent

The fix has a shape you will use constantly:

  1. Move the state up to the closest component that contains both.
  2. Pass the value down as a prop.
  3. Pass a function down so the child can ask for a change.

The child stops owning the value and becomes controlled by its parent:

import { useState } from "react";

// No state of its own: it renders what it is given, and reports clicks.
function Panel({ label, count, onIncrement }) {
  return (
    <div style={{ border: "1px solid #ddd", padding: 12, borderRadius: 6 }}>
      <p style={{ margin: "0 0 8px" }}>{label}: {count}</p>
      <button onClick={onIncrement}>+1</button>
    </div>
  );
}

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <div style={{ display: "flex", gap: 8 }}>
        <Panel label="Left" count={count} onIncrement={() => setCount(count + 1)} />
        <Panel label="Right" count={count} onIncrement={() => setCount(count + 1)} />
      </div>
      <p>The parent owns the count: {count}</p>
    </div>
  );
}

Data still flows one way. The child never reaches up — it calls a function the parent gave it, and the parent decides what that means. This is the pattern from Components and Props, now doing real work.

A worked example

Two inputs showing the same value in different units. Neither can own the temperature, because editing either must update both:

import { useState } from "react";

function TemperatureInput({ label, value, onChange }) {
  return (
    <p>
      <label>
        {label}:{" "}
        <input value={value} onChange={(e) => onChange(e.target.value)} style={{ width: 80 }} />
      </label>
    </p>
  );
}

export default function App() {
  // One source of truth. Everything else is derived from it.
  const [celsius, setCelsius] = useState("20");

  const asNumber = Number(celsius);
  const fahrenheit = Number.isNaN(asNumber) ? "" : String(asNumber * 9 / 5 + 32);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, fontSize: 13 }}>
      <TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} />
      <TemperatureInput
        label="Fahrenheit"
        value={fahrenheit}
        onChange={(f) => setCelsius(String((Number(f) - 32) * 5 / 9))}
      />
      <p>{asNumber >= 100 ? "Water would boil." : "Not boiling yet."}</p>
    </div>
  );
}

Note what is not state: fahrenheit is calculated during render from celsius. Storing both would let them disagree. Keep one source of truth and derive the rest — the same point as "you might not need an effect" in useEffect.

How far to lift

Lift to the closest common parent, and no further. Every level above that gets a prop it does not care about, and every state change re-renders a larger part of the tree.

When the distance is genuinely large — a theme, the signed-in user — the answer is not to lift through ten layers but useContext. When the transitions get complicated, useReducer in the parent keeps them in one place.

Common mistakes

  • Duplicating state in the child. Copying a prop into useState gives the child a stale copy that stops following the parent.
  • Lifting too far. State at the root that only two leaves use.
  • Storing what you can derive. Two values that must agree will eventually disagree.

Exercise

The search box filters nothing, because SearchInput keeps the query to itself while Results cannot see it. Lift the query into App so typing filters the list.

import { useState } from "react";

const fruits = ["Apple", "Apricot", "Banana", "Cherry", "Clementine"];

function SearchInput() {
  const [query, setQuery] = useState("");

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search fruit"
    />
  );
}

function Results({ query }) {
  const visible = fruits.filter((f) =>
    f.toLowerCase().startsWith(String(query).toLowerCase())
  );

  return <ul>{visible.map((f) => <li key={f}>{f}</li>)}</ul>;
}

export default function App() {
  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, fontSize: 13 }}>
      <SearchInput />
      <Results query="" />
    </div>
  );
}