LearnFrenzy
Sign in Take a free test

State and Events · Lesson 11 of 31

Lifting State Up in React

6 min read Updated Nov 8, 2026 Saurabh Samir

Sometimes two components need the same data: a filter box and the list it filters, or a cart icon and the product page. State lives in one component, so the answer is to move it up to their closest common parent and pass it down as props. This is called lifting state up, and it's the standard way React components share data.

In this lesson

  • The problem: siblings with separate state
  • Lifting state to the parent
  • Controlled vs uncontrolled components
  • Choosing where state should live

The problem

An FAQ where only one answer should be open at a time. If each panel keeps its own isOpen state, they can't know about each other:

import { useState } from "react";

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

export default function App() {
  return (
    <>
      <Panel title="Is it free?">Yes, every tutorial is free.</Panel>
      <Panel title="Do I get a certificate?">Yes, after the final quiz.</Panel>
    </>
  );
}

Result

Is it free?
[Show Is it free?]
Do I get a certificate?
[Show Do I get a certificate?]

› click “Show Is it free?”
Is it free?
Yes, every tutorial is free.
Do I get a certificate?
[Show Do I get a certificate?]

› click “Show Do I get a certificate?”
Is it free?
Yes, every tutorial is free.
Do I get a certificate?
Yes, after the final quiz.

Both end up open. To make them coordinate, the parent has to own the decision.

Lift the state to the parent

  1. Remove the state from the children.
  2. Add it to the common parent.
  3. Pass the value and an event handler down as props.
import { useState } from "react";

function Panel({ title, isOpen, onShow, children }) {   // no state: fully controlled by props
  return (
    <section>
      <h3>{title}</h3>
      {isOpen ? <p>{children}</p> : <button onClick={onShow}>Show {title}</button>}
    </section>
  );
}

export default function App() {
  const [openIndex, setOpenIndex] = useState(0);   // the single source of truth
  return (
    <>
      <Panel title="Is it free?" isOpen={openIndex === 0} onShow={() => setOpenIndex(0)}>
        Yes, every tutorial is free.
      </Panel>
      <Panel title="Do I get a certificate?" isOpen={openIndex === 1} onShow={() => setOpenIndex(1)}>
        Yes, after the final quiz.
      </Panel>
    </>
  );
}

Result

Is it free?
Yes, every tutorial is free.
Do I get a certificate?
[Show Do I get a certificate?]

› click “Show Do I get a certificate?”
Is it free?
[Show Is it free?]
Do I get a certificate?
Yes, after the final quiz.

› click “Show Is it free?”
Is it free?
Yes, every tutorial is free.
Do I get a certificate?
[Show Do I get a certificate?]

Data down, events up

  1. App owns openIndexthe single source of truth
  2. Props downeach Panel gets isOpen and onShow
  3. Event upa click calls onShow → setOpenIndex(1)
  4. Re-renderboth panels receive new isOpen values

A search box and a list

import { useState } from "react";

const courses = ["React.js", "Next.js", "TypeScript", "JavaScript", "Node.js"];

function SearchBar({ query, onQueryChange }) {
  return <input placeholder="Filter courses" value={query} onChange={(e) => onQueryChange(e.target.value)} />;
}

function CourseList({ query }) {
  const matches = courses.filter((c) => c.toLowerCase().includes(query.toLowerCase()));
  return (
    <>
      <p>{matches.length} of {courses.length} courses</p>
      <ul>{matches.map((c) => <li key={c}>{c}</li>)}</ul>
    </>
  );
}

export default function App() {
  const [query, setQuery] = useState("");          // lifted: both children need it
  return (
    <>
      <SearchBar query={query} onQueryChange={setQuery} />
      <CourseList query={query} />
    </>
  );
}

Result

[Filter courses…]
5 of 5 courses
• React.js
• Next.js
• TypeScript
• JavaScript
• Node.js

› type “js” into Filter courses
[js]
3 of 5 courses
• React.js
• Next.js
• Node.js

Controlled vs uncontrolled

The first Panel was uncontrolled: it owned its state and the parent couldn't influence it. The lifted Panel is controlled: its important values come from props. Controlled components are more flexible (the parent can coordinate them); uncontrolled ones are simpler to drop in. Many good components support both, like an <input> with either value or defaultValue.

Where should state live?

A quick checklist

  1. List every component that reads or changes the value.
  2. Find their closest common parent – the state goes there (or just above).
  3. If that means passing props through many layers that don't use them ("prop drilling"), consider composition with children or Context – covered later in this course.

Keep state as low as possible, though. A text input's draft value that nobody else cares about should stay inside that input's component – lifting everything to the top makes the whole app re-render on every keystroke.

Practice

Try it yourself

  1. Build a temperature converter: two inputs (Celsius and Fahrenheit) that always stay in sync.

    Show solution
    import { useState } from "react";
    
    function TempInput({ label, value, onChange }) {
      return (
        <label>
          {label} <input aria-label={label} value={value} onChange={(e) => onChange(e.target.value)} />
        </label>
      );
    }
    
    export default function App() {
      const [celsius, setCelsius] = useState("25");    // one source of truth, in Celsius
      const c = parseFloat(celsius);
      const fahrenheit = Number.isNaN(c) ? "" : String(Math.round((c * 9) / 5 + 32));
    
      return (
        <div>
          <TempInput label="Celsius" value={celsius} onChange={setCelsius} />
          <TempInput
            label="Fahrenheit"
            value={fahrenheit}
            onChange={(f) => setCelsius(f === "" ? "" : String(Math.round(((parseFloat(f) - 32) * 5) / 9)))}
          />
        </div>
      );
    }

    Result

    Celsius [25] Fahrenheit [77]
    
    › clear Celsius and type “100”
    Celsius [100] Fahrenheit [212]
    
    › clear Fahrenheit and type “32”
    Celsius [0] Fahrenheit [32]

    Only Celsius is stored; Fahrenheit is derived. Storing both would let them disagree.

Quick check

Where do you put state that two siblings need?

In their closest common parent, passed down as props.

How does a child change lifted state?

By calling a function prop the parent passes down.

What's a controlled component?

One whose important values are driven by props rather than its own state.

Summary

  • To share state, lift it to the closest common parent and pass it down.
  • Children report changes through function props: data down, events up.
  • Keep one source of truth, derive the rest, and keep state as low as it can go.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links