Skip to content
React rc state-events 4 min read

Lifting State Up

When two or more components need to reflect the same changing data, the cleanest solution in React is to move that state up to their closest common ancestor. This pattern is called lifting state up. Instead of each component holding its own copy of the data, the shared value lives in one parent, which passes it down as props and exposes setters as callbacks. The result is a single source of truth that keeps every child perfectly in sync.

Why state ends up in the wrong place

It is natural to declare state inside the component that uses it. But the moment a sibling component needs to read or change that same value, local state becomes a problem: siblings cannot talk to each other directly in React. Data flows down the tree through props, never sideways. So if two siblings must agree on a value, the value has to live somewhere they can both see it — their common parent.

Rule of thumb: state should live at the lowest common ancestor of every component that reads or writes it. No higher, no lower.

Passing state down, lifting events up

Lifting state has two halves that work together:

  • State flows down as props. The parent owns the value and hands the current value to each child.
  • Events flow up as callbacks. Children cannot mutate the parent’s state directly, so the parent passes down setter functions. A child calls the callback, the parent updates its state, and React re-renders both children with the new value.

This round trip — child calls a function, parent updates state, new props flow back down — is often called inverse data flow. The data still only flows one way (down), but a child can request a change by invoking a callback the parent gave it.

Worked example: two synced inputs

Imagine a temperature converter with a Celsius field and a Fahrenheit field. Typing in either one should instantly update the other. If each input owned its own state, they could drift apart. By lifting the temperature into the parent, both inputs always reflect a single value.

import { useState } from "react";

function TemperatureInput({ scale, value, onChange }) {
  const label = scale === "c" ? "Celsius" : "Fahrenheit";

  return (
    <fieldset>
      <legend>Enter temperature in {label}:</legend>
      <input
        type="number"
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
    </fieldset>
  );
}

function toCelsius(f) {
  return ((f - 32) * 5) / 9;
}

function toFahrenheit(c) {
  return (c * 9) / 5 + 32;
}

function tryConvert(value, convert) {
  const input = parseFloat(value);
  if (Number.isNaN(input)) return "";
  const output = convert(input);
  return (Math.round(output * 1000) / 1000).toString();
}

export default function Calculator() {
  const [temperature, setTemperature] = useState("");
  const [scale, setScale] = useState("c");

  const celsius =
    scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
  const fahrenheit =
    scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;

  return (
    <div>
      <TemperatureInput
        scale="c"
        value={celsius}
        onChange={(value) => {
          setScale("c");
          setTemperature(value);
        }}
      />
      <TemperatureInput
        scale="f"
        value={fahrenheit}
        onChange={(value) => {
          setScale("f");
          setTemperature(value);
        }}
      />
      <p>
        {tryConvert(temperature, scale === "c" ? toCelsius : toFahrenheit) === ""
          ? "Enter a number to see the conversion."
          : "Both fields stay in sync."}
      </p>
    </div>
  );
}

Here the Calculator parent holds two pieces of state: the raw temperature value and which scale the user last edited. Each TemperatureInput is fully controlled — it receives its displayed value as a prop and reports edits through onChange. Neither input owns the data, so they can never disagree.

Output:

Typing "100" into the Celsius field instantly shows "212" in Fahrenheit.
Clearing a field clears both.

What to keep where

A useful way to decide what becomes state versus what is computed:

ConcernWhere it livesWhy
The raw value the user typedState in the parentIt is the single source of truth
Which input was last editedState in the parentDetermines conversion direction
The converted value shown in the other fieldDerived during renderIt is computed from state, not stored
Each input’s current display valueProps passed from parentChildren only render what they are given

Notice we store the minimal state and derive everything else. Storing both temperatures separately would create two sources of truth and force you to keep them manually in sync — the exact bug lifting state is meant to prevent.

Lifting too high

Lifting state has a cost: every change re-renders the owning parent and all of its children. If you lift state higher than necessary, unrelated parts of the tree re-render and prop-drilling gets tedious. Lift only as high as the closest common ancestor. When state truly needs to be shared across many distant components, reach for Context or a state library instead of threading props through a dozen layers.

Gotcha: if you find yourself passing the same prop through several intermediate components that do not use it, you have likely lifted too high. Consider Context.

Best practices

  • Identify every component that reads or writes a value, then place the state at their lowest common ancestor.
  • Keep state minimal: store only raw values and derive the rest during render.
  • Pass the current value down as a prop and pass a setter or handler down as a callback.
  • Make shared inputs fully controlled so the parent stays the single source of truth.
  • Avoid duplicating the same data into multiple useState hooks across siblings.
  • Do not lift state higher than the closest common ancestor; reach for Context when props would travel through many layers.
Last updated June 14, 2026
Was this helpful?