Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 18 of 38

Lifting state up

Share state between sibling components by moving it to their closest common parent, pass the value and the setter down as props, and keep one source of truth.

16 minState and eventsUpdated 24 October 2026

By the end of this lessonMove state to a common parent so that sibling components stay in sync, and explain controlled and uncontrolled components.

Each component has its own state, but often two components need to show or change the same data: two panels that react to the same filter, a list and a detail view, a field that appears in two places. A component cannot reach into its sibling's state, and it should not. The solution is one of React's most useful patterns: lift the state up to the nearest component that contains both.

The problem: two independent copies

Here are two panels, each with its own "active" flag. Open one and the other does not close, because they know nothing about each other:

jsx
import { useState } from "react";

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section>
      <h3>{title}</h3>
      {isActive ? <p>{children}</p> : <button onClick={() => setIsActive(true)}>Show</button>}
    </section>
  );
}

function App() {
  return (
    <div>
      <Panel title="About">A small online course platform.</Panel>
      <Panel title="Contact">Write to us anytime.</Panel>
    </div>
  );
}

export default App;
result
About
A small online course platform.
Contact
Write to us anytime.

Both panels ended up open. Suppose we want an accordion: only one open at a time. Each panel cannot decide that alone, because it needs to know about the other. The decision belongs to the parent.

Step 1: remove the state from the children

Make Panel a controlled component: instead of keeping isActive itself, it receives isActive and an onShow function as props, and renders accordingly.

Step 2: put the state in the parent

The parent, Accordion, keeps one piece of state: which panel is open (an index, or null). It passes each panel whether it is active, and a function to open it:

jsx
import { useState } from "react";

function Panel({ title, children, isActive, onShow }) {
  return (
    <section>
      <h3>{title}</h3>
      {isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
    </section>
  );
}

function App() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <div>
      <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>
        A small online course platform.
      </Panel>
      <Panel title="Contact" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>
        Write to us anytime.
      </Panel>
    </div>
  );
}

export default App;
result
About
Show
Contact
Write to us anytime.

Opening "Contact" closes "About" because there is only one activeIndex. Notice what flows where:

  • Down: isActive and onShow are passed from the parent to each panel as props.
  • Up: when a button is pressed, the panel calls onShow(). That runs in the parent, which updates its own state. React renders the parent again, and both panels receive new props.

This is the heart of React's data flow: props flow down, events flow up. A child never changes a parent's data directly; it only calls a function the parent gave it.

A second example: two inputs, one value

The classic lifting example is two fields that show the same value in different units. Neither owns the temperature; the parent does:

jsx
import { useState } from "react";

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

const toFahrenheit = c => Math.round((c * 9) / 5 + 32);
const toCelsius = f => Math.round(((f - 32) * 5) / 9);

function App() {
  const [celsius, setCelsius] = useState("");

  const fahrenheit = celsius === "" || isNaN(celsius) ? "" : String(toFahrenheit(Number(celsius)));

  function handleFahrenheit(text) {
    setCelsius(text === "" || isNaN(text) ? "" : String(toCelsius(Number(text))));
  }

  return (
    <div>
      <div className="c"><TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} /></div>
      <div><TemperatureInput label="Fahrenheit" value={fahrenheit} onChange={handleFahrenheit} /></div>
      <p>Celsius {celsius || "?"} = Fahrenheit {fahrenheit || "?"}</p>
    </div>
  );
}

export default App;
result
Celsius: 
Fahrenheit: 
Celsius 100 = Fahrenheit 212

In the test, typing 100 in the Celsius box filled the Fahrenheit box with 212, as the line at the bottom confirms. Only one value, celsius, is stored. The Fahrenheit value is derived from it during rendering, and typing in the Fahrenheit box converts back and updates celsius. The two fields can never disagree, because there is a single source of truth. If each input had its own state, you would need code to copy values between them and make sure they never conflict.

Controlled and uncontrolled components

A component with an important piece of data can be built in two ways:

Uncontrolled Controlled
Where the value lives in the component's own state in the parent, passed as a prop
Changes are reported by usually not (the parent cannot see them) callbacks such as onChange
Easier to use and drop in coordinate with other components

Neither is wrong. An isolated widget with a private state is simpler. As soon as a parent needs to read or reset the value, or sync it with another component, make it controlled. A good habit when writing a reusable component is to support both: use the prop if it is given, and fall back to your own state.

Do not copy props into state

A tempting shortcut is to copy a prop into the child's state to "have it locally":

jsx
function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);   // captured once, on the first render only
  // ...
}

If the parent later passes a different messageColor, the child ignores it, because useState only uses its argument for the first render. Use the prop directly (const color = messageColor), or, if you really need an editable local copy that starts from the prop, name the prop initialColor so it is clear that later changes are ignored.

How high should state go?

Follow these steps when two components need the same data:

  1. Find their closest common parent. That is the lowest component that contains both.
  2. Move the state there, and pass the value (and the setter or an onSomething function) down.
  3. Remove the state from the children, so there is one source of truth.

Do not lift further than necessary. State in App that only one subtree needs makes unrelated components re-render and makes the code harder to follow. Move it down again when a component stops sharing it. If you find yourself passing the same props through many layers, the later lesson on context offers another way.

Common mistakes

  • Keeping a copy of the same data in two components.
  • Passing the value but forgetting to pass the function to change it, leaving the child unable to update anything.
  • Having the child modify a prop instead of calling the parent's function.
  • Lifting state to the very top by default.
  • Copying a prop into state and expecting it to follow later prop changes.
  • Calling the handler directly in JSX: onShow={setActiveIndex(1)} instead of onShow={() => setActiveIndex(1)}.

Exercise

  1. Add a third panel to the accordion and change the logic so that clicking the open panel's heading closes it (state can be null).
  2. Build a SearchBox and a ResultList. Put the search text state in their common parent so the list filters as the user types.
  3. Make a Counter component that is controlled: it takes value and onChange props, and a parent shows two counters that share one value.
  4. Take the Celsius/Fahrenheit example and add a third field for Kelvin using the same single source of truth.

Key points

  • When two components need to reflect the same data, move the state up to their closest common parent and pass it down as props.
  • The parent passes both the value and a function (such as onChange) so children can ask for changes; data flows down, events flow up.
  • A component whose value is controlled by props is a controlled component; one that keeps its own state is uncontrolled.
  • Keep one source of truth for each piece of data, and put state in the lowest component that needs it.

Check yourself

1. Two sibling components must show the same selected item. Where should the state live?
2. How does a child change state that lives in its parent?
3. What is a controlled component?
4. What is wrong with copying a parent's prop into the child's own state?