Here is a filter box and a list. The box knows what was typed. The list needs to know it
too, so it can filter itself.
function SearchBox() {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
function NoteList({ notes }) {
// ...and how does this get the query?
}
It cannot. query is inside SearchBox, and lesson 4’s rule still holds: data goes down,
never up or sideways. A sibling has no way to see it.
State that two components share does not belong to either of them.
Move the state to the nearest common parent
A value two components share belongs to the closest component that contains them both:
import { useState } from "react";
const allNotes = [
{ id: "a1", title: "Monday", body: "Started the course." },
{ id: "b2", title: "Tuesday", body: "Got as far as props." },
{ id: "c3", title: "Wednesday", body: "Lists, apparently." },
];
function SearchBox({ query, onQueryChange }) {
return (
<input
value={query}
onChange={(e) => onQueryChange(e.target.value)}
placeholder="Filter notes"
/>
);
}
function NoteList({ notes }) {
if (notes.length === 0) {
return <p>Nothing matches.</p>;
}
return (
<ul>
{notes.map((note) => (
<li key={note.id}>
<strong>{note.title}</strong> {note.body}
</li>
))}
</ul>
);
}
function App() {
const [query, setQuery] = useState("");
const visible = allNotes.filter((note) =>
note.title.toLowerCase().includes(query.toLowerCase()),
);
return (
<main>
<SearchBox query={query} onQueryChange={setQuery} />
<NoteList notes={visible} />
</main>
);
}
export default App;
Type in the box and the list filters.
SearchBox no longer has state. It receives the value it should display and a function to
call when the user changes it. NoteList never hears about the query at all - it receives
a list and renders it. Neither one knows the other exists.
App holds the state, so it can hand pieces of it to both children. That is the whole
technique.
The child talks back by calling a function
Data still only flows downward. What flows down here is a function, and calling it is how
a child reports something upward without ever reaching up.
<SearchBox query={query} onQueryChange={setQuery} />
setQuery is just a value, and props can be any value. SearchBox calls it, App’s state
changes, App re-renders, and both children get fresh props. The child did not update the
parent - it asked, and the parent decided.
Two conventions worth adopting, because everyone else has:
- Name the prop for the event:
onQueryChange, onSelect, onDelete.
- Name the handler for what it does:
handleQueryChange, handleDelete.
You can pass setQuery directly, as above, when the child’s value is exactly the state.
Once anything else needs to happen, write a real function:
function handleQueryChange(next) {
setQuery(next);
setPage(1); // filtering should send you back to page one
}
Do not store what you can calculate
Look again at visible:
const visible = allNotes.filter((note) =>
note.title.toLowerCase().includes(query.toLowerCase()),
);
That is a plain const, recalculated on every render. It is deliberately not state, and
this is the part people get wrong.
The tempting version looks like this:
const [visible, setVisible] = useState(allNotes); // NO
function handleQueryChange(next) {
setQuery(next);
setVisible(allNotes.filter(/* ... */));
}
Now the same fact lives in two places, and they can disagree. Add a second way to change
the notes - a delete button, say - and you have to remember to re-filter there too. Miss
it once and the screen shows a note that no longer exists. This is precisely the bug from
lesson 1, rebuilt with React parts.
So: if you can compute it from state and props during render, do not put it in state.
The filtered list, the total, whether the form is valid, whether anything is selected -
all derived. State should hold the smallest set of facts that cannot be worked out from
anything else. Here that is one string.
The question to ask of any candidate piece of state is: could I work this out from
something else I already have? If yes, work it out.
Where does it stop?
Lifting has a cost. Every level you move state up is another level of props to thread
through, and the parent grows more entangled with children that used to be independent.
The rule is the nearest common parent, not the top. If two things in a sidebar need to
agree, the state belongs to the sidebar, not to App. Moving it higher than necessary
gives you a component holding facts that nothing near it uses, and a trail of props
carrying values through components that only pass them along.
When threading really does get painful across many levels, there is a tool for it -
useContext - and lesson 12 says where it fits. Know that it exists, and leave it there
for now: most apps need lifting far more often than they need context, and context used
early hides exactly the data flow that makes React readable.
Recap
State that two components share belongs to their nearest common parent, which passes the
value down to one and a function down to the other. The child never reaches upward; it
calls a function it was given, and the parent decides what to do. Anything you can
calculate from state during render should be calculated, not stored, because two copies of
one fact will eventually disagree.
Next: talking to the world outside React - fetching data, and the hook that everyone
learns the wrong way round.