Sharing State Between Components

React · Lesson 12 of 29 6 min read Updated

Sometimes two components need the same data. A search box and a results list must agree on the search text. In an accordion, opening one panel should close the others. If each component keeps its own copy of the state, they cannot stay in sync.

The React solution is called lifting state up: move the state to the closest common parent, and pass it down to the children as props, together with functions to change it. In this lesson you will lift state step by step, learn the idea of a "single source of truth", and see where this pattern starts to feel heavy.

The problem: separate state, separate worlds

Here is an FAQ section for a coaching institute. Each Panel has its own isOpen state, so the panels know nothing about each other and several can be open at once:

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

That is fine for some designs. But suppose the requirement is "only one answer open at a time". A panel cannot close its sibling, because it has no access to the sibling's state. The information has to live somewhere both panels can reach.

Lifting the state up

Lifting state up takes three steps:

  1. Remove the state from the children.
  2. Add it to the closest common parent.
  3. Pass the value down as a prop, plus a handler function so children can ask the parent to change it.
React JSXTry it yourself
import { useState } from "react";
import { createRoot } from "react-dom/client";

function Panel({ title, isOpen, onShow, children }) {
  return (
    <section style={{ borderBottom: "1px solid #ddd", padding: "4px 0" }}>
      <h3 style={{ margin: "6px 0" }}>{title}</h3>
      {isOpen ? <p>{children}</p> : <button onClick={onShow}>Show answer</button>}
    </section>
  );
}

function Faq() {
  const [openIndex, setOpenIndex] = useState(0);

  return (
    <>
      <h2>Admissions FAQ, Pune centre</h2>
      <Panel title="When do new batches start?" isOpen={openIndex === 0} onShow={() => setOpenIndex(0)}>
        New weekday batches start on the first Monday of every month.
      </Panel>
      <Panel title="Can I pay the fee in instalments?" isOpen={openIndex === 1} onShow={() => setOpenIndex(1)}>
        Yes, the fee can be paid in two instalments.
      </Panel>
      <Panel title="Are weekend classes available?" isOpen={openIndex === 2} onShow={() => setOpenIndex(2)}>
        Yes, on Saturdays and Sundays from 10 am.
      </Panel>
    </>
  );
}

createRoot(document.getElementById("root")).render(<Faq />);

Now Faq owns one piece of state, openIndex, and each Panel is told whether it is open. When a panel's button is clicked, it calls onShow, and the parent updates openIndex. Because there is only one value, only one panel can ever be open.

Panel has become a controlled component: its important behaviour is decided by props from the parent, not by its own state. A component with its own internal state is called uncontrolled. Both are useful. Uncontrolled components are simpler to use; controlled ones are more flexible, because the parent can coordinate them.

A single source of truth

The guiding principle is: for each piece of changing data, exactly one component owns it. Other components receive it through props. This is called having a single source of truth. It means you never have two copies of the same data that might disagree.

Here is a hostel room finder. The search text and the "AC only" filter are needed by both the filter bar (to show the current values) and the room list (to decide what to show), so they live in the parent:

React JSXTry it yourself
import { useState } from "react";
import { createRoot } from "react-dom/client";

const rooms = [
  { id: 1, name: "Ganga Block 101", beds: 2, ac: true, rent: 7500 },
  { id: 2, name: "Ganga Block 204", beds: 3, ac: false, rent: 4800 },
  { id: 3, name: "Yamuna Block 12", beds: 1, ac: true, rent: 9200 },
  { id: 4, name: "Kaveri Block 7", beds: 2, ac: false, rent: 5200 },
];

function FilterBar({ query, onQueryChange, acOnly, onAcOnlyChange }) {
  return (
    <div>
      <input value={query} onChange={(e) => onQueryChange(e.target.value)} placeholder="Search block" />
      <label>
        <input type="checkbox" checked={acOnly} onChange={(e) => onAcOnlyChange(e.target.checked)} /> AC only
      </label>
    </div>
  );
}

function RoomList({ rooms }) {
  if (rooms.length === 0) return <p>No rooms match.</p>;
  return (
    <ul>
      {rooms.map((r) => (
        <li key={r.id}>
          {r.name}: {r.beds} bed(s), {r.ac ? "AC" : "non-AC"}, ₹{r.rent.toLocaleString("en-IN")}/month
        </li>
      ))}
    </ul>
  );
}

function RoomFinder() {
  const [query, setQuery] = useState("");
  const [acOnly, setAcOnly] = useState(false);

  const visible = rooms.filter(
    (r) => r.name.toLowerCase().includes(query.toLowerCase()) && (!acOnly || r.ac)
  );

  return (
    <>
      <FilterBar query={query} onQueryChange={setQuery} acOnly={acOnly} onAcOnlyChange={setAcOnly} />
      <p>{visible.length} of {rooms.length} rooms</p>
      <RoomList rooms={visible} />
    </>
  );
}

createRoot(document.getElementById("root")).render(<RoomFinder />);

Notice what is not in state: the filtered list visible. It is calculated during each render from rooms, query and acOnly. Storing it in state as well would create a second copy that you must remember to update. Keep state minimal and derive everything else.

Also notice that FilterBar never imports or calls useState for these values. It only displays props and reports changes upward with onQueryChange and onAcOnlyChange. You could reuse it anywhere a parent wants to own the filter values.

How to decide where state lives

For each piece of state, ask:

  • Which components read this value?
  • What is their closest common parent?
  • Put the state there, or higher if a component above also needs it.

Data then flows down through props, and changes flow up through handler functions. This one-way flow makes bugs easier to trace: to find out why a value is wrong, you look at the one component that owns it.

When lifting gets heavy

Lifting works well for a few levels. But if the owner is far above the components that use the value, you end up passing the same prop through many components that do not use it themselves, only to hand it further down:

React JSX
<App user={user}>          {/* owns user */}
  <Layout user={user}>     {/* does not use it */}
    <Sidebar user={user}>  {/* does not use it */}
      <Avatar user={user} />  {/* finally uses it */}

This is called prop drilling. For data that many distant components need, such as the logged-in user, the theme or the language, React offers Context. For complex update logic, useReducer helps. Try lifting first; it is often enough.

Common mistakes

  • Copying props into state. const [query, setQuery] = useState(props.query) creates a second copy that ignores later prop changes. Use the prop directly.
  • Keeping derived data in state. Filtered lists, totals and counts should be calculated during render.
  • Lifting too high. State that only one small component uses should stay in that component.
  • Changing a prop instead of calling the handler. A child must call the parent's function (onQueryChange(value)); it cannot change the parent's state directly.

Key points

  • When two components need the same changing data, lift the state to their closest common parent.
  • Pass the value down as a prop and a handler (onSomething) to change it.
  • Each piece of state should have one owner: a single source of truth.
  • Keep state minimal; calculate derived values such as filtered lists during render.
  • Data flows down through props; changes flow up through handler calls.
  • Deep prop drilling is a sign to consider Context.

Exercise

Build a TemperatureBoard with two inputs: one in Celsius and one in Fahrenheit. Typing in either input must update the other. Hint: store one value and which unit it was typed in, in the parent, and calculate the other value during render. Use F = C x 9 / 5 + 32.

Show solution
React JSX
import { useState } from "react";
import { createRoot } from "react-dom/client";

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

function TemperatureBoard() {
  const [temp, setTemp] = useState({ value: "40", unit: "c" });
  const n = parseFloat(temp.value);
  const toF = (c) => (c * 9) / 5 + 32;
  const toC = (f) => ((f - 32) * 5) / 9;

  const celsius = temp.unit === "c" ? temp.value : Number.isNaN(n) ? "" : String(Math.round(toC(n) * 10) / 10);
  const fahrenheit = temp.unit === "f" ? temp.value : Number.isNaN(n) ? "" : String(Math.round(toF(n) * 10) / 10);

  return (
    <>
      <h3>Jaipur afternoon temperature</h3>
      <TempInput label="Celsius" value={celsius} onChange={(v) => setTemp({ value: v, unit: "c" })} />
      <TempInput label="Fahrenheit" value={fahrenheit} onChange={(v) => setTemp({ value: v, unit: "f" })} />
    </>
  );
}

createRoot(document.getElementById("root")).render(<TemperatureBoard />);

The parent stores only what the user typed and its unit. The other input's value is derived, so the two can never disagree.

Quick Quiz

1. Two sibling components need the same state. Where should it live?

2. How does a child ask the parent to change lifted state?

3. A list is filtered by a search text in state. Should the filtered list also be state?

4. What is prop drilling?

Learn React with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call