Lifting state up
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
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
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, andonShow, a function it calls to ask the parent for a change.
App (owns openIndex)
/ | \
props: / | \ isOpen, onShow
v v v
Panel Panel Panel a click calls onShow(),
which updates App's stateData 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
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
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.