Sharing state between components
Lift state up to a common parent so that two components always stay in sync.
Sometimes two components need to change together. Picture an accordion: when you open one panel, the others should close. If each panel keeps its own isOpen state, they have no way to know about each other.
The fix is called lifting state up: remove the state from the children, move it to their closest common parent, and pass it back down through props. It is the most common thing you will do with state in React.
The problem: independent state
Here each Panel owns its own isActive state. Click "Show" on both. They both open, because neither knows the other exists.
The goal: only one panel open at a time.
Lifting state up, in three steps
Step 1. Remove state from the child. Delete the useState line from Panel and receive isActive as a prop instead. Now the parent decides.
Step 2. Pass hard-coded data from the parent. In Accordion, pass isActive={true} or isActive={false} to each Panel to check that the props work.
Step 3. Add state to the parent. Store which panel is open as a single number, activeIndex. Pass a callback so a panel can ask the parent to change it.
Each Panel is now "dumb": it shows what it is told and reports clicks through onShow. The parent is the single source of truth for which panel is open.
Controlled and uncontrolled components
A component with its own local state is uncontrolled: its parent can't influence it. The first Panel was uncontrolled. A component whose important data comes from props is controlled: the parent fully decides how it behaves. The second Panel is controlled.
You met the same idea with form inputs on Day 3. An <input value={text} onChange={...} /> is a controlled input. Uncontrolled components are easier to drop in, and controlled components are more flexible. When you build a component, decide which values should be props (controlled) and which should be local state (uncontrolled).
You lifted isActive from Panel into Accordion. How does a Panel now open itself?
Project: a FilterBar for the Reading List
Your Reading List needs filter buttons: All, Want, Reading and Done. Two components care about the filter:
FilterBarshows the buttons and highlights the active one.BookListonly shows matching books.
So the filter state lives in their shared parent, App. The filtered list is derived during render, not stored.
FilterBar is fully controlled. It doesn't know how filtering works; it shows filter and calls onFilterChange. The aria-pressed attribute tells screen readers which button is on.
Show a count on each filter button
Make each button show how many books match it, like "Want (2)". FilterBar needs the books to count them. Should the counts be new state?
Show solution
No new state: the counts can be calculated from books. Pass books to FilterBar and count during render.
const FILTERS = [
{ value: 'all', label: 'All' },
{ value: 'want', label: 'Want' },
{ value: 'reading', label: 'Reading' },
{ value: 'done', label: 'Done' },
];
export function FilterBar({ books, filter, onFilterChange }) {
return (
<div className="filter-bar" role="group" aria-label="Filter books">
{FILTERS.map((f) => {
const count =
f.value === 'all' ? books.length : books.filter((b) => b.status === f.value).length;
return (
<button
key={f.value}
aria-pressed={filter === f.value}
className={filter === f.value ? 'active' : ''}
onClick={() => onFilterChange(f.value)}
>
{f.label} ({count})
</button>
);
})}
</div>
);
}In App, render <FilterBar books={books} filter={filter} onFilterChange={setFilter} />.
Day 4 progress