CodeMIRAI
0 XP

Lifting state up

3 min read 4 runnable examples Quiz included

When two components need the same data, neither should keep its own copy. Move the state up to their closest shared parent, and pass it down to both.

The problem

React (JSX)
import { useState } from "react";

function Panel({ title, children }) {
  const [open, setOpen] = useState(false);
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, margin: "6px 0", padding: "8px 12px" }}>
      <strong>{title}</strong>{" "}
      <button onClick={() => setOpen(!open)}>{open ? "Hide" : "Show"}</button>
      {open && <p>{children}</p>}
    </div>
  );
}

function App() {
  return (
    <div>
      <Panel title="What is React?">A library for building user interfaces.</Panel>
      <Panel title="What is state?">Data a component remembers between renders.</Panel>
      <Panel title="What are props?">Inputs passed to a component by its parent.</Panel>
    </div>
  );
}

Each panel has its own open state, so several can be open at once. To allow only one open panel, the panels would need to know about each other, and they can't: each one's state is private.

Lifting the state up

React (JSX)
import { useState } from "react";

function Panel({ title, isOpen, onShow, children }) {
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, margin: "6px 0", padding: "8px 12px" }}>
      <strong>{title}</strong>{" "}
      {!isOpen && <button onClick={onShow}>Show</button>}
      {isOpen && <p>{children}</p>}
    </div>
  );
}

function App() {
  const [openIndex, setOpenIndex] = useState(0);
  const faqs = [
    ["What is React?", "A library for building user interfaces."],
    ["What is state?", "Data a component remembers between renders."],
    ["What are props?", "Inputs passed to a component by its parent."],
  ];
  return (
    <div>
      {faqs.map(([question, answer], i) => (
        <Panel key={question} title={question} isOpen={openIndex === i} onShow={() => setOpenIndex(i)}>
          {answer}
        </Panel>
      ))}
    </div>
  );
}

Now App owns one piece of state, openIndex, and each Panel receives:

  • isOpen, a value passed down, and
  • onShow, a function it calls to ask the parent for a change.
Diagram
              App   (owns openIndex)
            /   |   \
   props:  /    |    \   isOpen, onShow
          v     v     v
       Panel  Panel  Panel     a click calls onShow(),
                               which updates App's state

Data flows down as props, and requests to change it flow up as callbacks. The panels no longer hold state of their own; they just display what they're given.

Two inputs that stay in sync

React (JSX)
import { useState } from "react";

const toF = (c) => (c * 9) / 5 + 32;
const toC = (f) => ((f - 32) * 5) / 9;

function convert(value, fn) {
  const n = parseFloat(value);
  return Number.isNaN(n) ? "" : String(Math.round(fn(n) * 10) / 10);
}

function TemperatureInput({ label, value, onChange }) {
  return (
    <label style={{ display: "block", margin: "6px 0" }}>
      {label}: <input value={value} onChange={(e) => onChange(e.target.value)} size={8} />
    </label>
  );
}

function App() {
  const [temp, setTemp] = useState({ scale: "c", value: "100" });
  const celsius = temp.scale === "c" ? temp.value : convert(temp.value, toC);
  const fahrenheit = temp.scale === "f" ? temp.value : convert(temp.value, toF);

  return (
    <div>
      <TemperatureInput label="Celsius" value={celsius} onChange={(v) => setTemp({ scale: "c", value: v })} />
      <TemperatureInput label="Fahrenheit" value={fahrenheit} onChange={(v) => setTemp({ scale: "f", value: v })} />
      <p>{parseFloat(celsius) >= 100 ? "Water would boil." : "Water would not boil."}</p>
    </div>
  );
}

Type in either box, and the other updates. App stores only what was typed last, and in which scale; the other box's value is calculated. Storing both temperatures in separate state would let them disagree. One source of truth, everything else derived from it, is the heart of this lesson.

Siblings sharing state

React (JSX)
import { useState } from "react";

const PRODUCTS = ["Keyboard", "Mouse", "Monitor", "Webcam", "Headphones", "Microphone"];

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

function ProductList({ query }) {
  const matches = PRODUCTS.filter((p) => p.toLowerCase().includes(query.toLowerCase()));
  return <ul>{matches.map((p) => <li key={p}>{p}</li>)}</ul>;
}

function App() {
  const [query, setQuery] = useState("");
  return (
    <div>
      <SearchBar query={query} onQueryChange={setQuery} />
      <ProductList query={query} />
      <p style={{ color: "#888" }}>Searching for: "{query}"</p>
    </div>
  );
}

SearchBar and ProductList are siblings, so neither can reach the other's state. Their parent holds the query and hands it to both.

Finished this lesson?Mark it complete to earn +20 XP and move your progress on.