How two siblings share one value
Unit 3 established that props flow parent to child and are read-only, and unit 4 established that state is owned by one component. So a SearchBox and a ResultsList that are siblings cannot pass the query text between themselves: the query must live in state in their common parent, which passes it down to both.
Data flows down, and a sibling cannot reach sideways into another sibling. The shared value moves up into the closest common parent's state, and both children receive it as props.
That move is called lifting state up, and the name describes the refactor rather than a feature. Nothing new is imported, and a useState call simply moves from a child to a parent, with the value coming back down as props.
Lifting state up
The rule: state lives in the closest common parent of every component that needs it. The parent owns the value, children get it as props, and children that need to change it get a function prop.
function SearchPage({ products }) { const [query, setQuery] = useState(""); return ( <> <SearchBox query={query} onQueryChange={setQuery} /> <ResultsList products={products} query={query} /> </> ); } function SearchBox({ query, onQueryChange }) { return ( <input value={query} onChange={e => onQueryChange(e.target.value)} /> ); } function ResultsList({ products, query }) { const shown = products.filter(p => p.name.toLowerCase().includes(query.toLowerCase()) ); return <ul>{shown.map(p => <li key={p.id}>{p.name}</li>)}</ul>; }
Typing in the box calls onQueryChange, which is the parent's setQuery. The parent re-renders, and BOTH children receive the new query. One source of truth, two consumers, the controlled-input idea from lesson 6-1 applied between components.
The search page as runnable JavaScript
The parent owns query, both children receive it as an argument, and onQueryChange plays the function prop that carries changes up before re-rendering.
const products = [ { id: 1, name: "Keyboard" }, { id: 2, name: "Kettle" }, { id: 3, name: "Mouse" }, ]; let query = ""; // the lifted state, owned by the "parent" function searchBox(query) { return '<input value="' + query + '" />'; } function resultsList(products, query) { const shown = products.filter(p => p.name.toLowerCase().includes(query.toLowerCase()) ); return "<ul>" + shown.map(p => "<li>" + p.name + "</li>").join("") + "</ul>"; } function searchPage() { return searchBox(query) + resultsList(products, query); } function onQueryChange(next) { query = next; console.log(searchPage()); } console.log(searchPage()); onQueryChange("ke");
Output
<input value="" /><ul><li>Keyboard</li><li>Kettle</li><li>Mouse</li></ul> <input value="ke" /><ul><li>Keyboard</li><li>Kettle</li></ul>
The filter is case-insensitive on both sides, lowercasing the product name and the query, which is why "ke" matches Keyboard. Lowercasing only one side is the usual bug, and it makes search work for lowercase typing and fail for anything else.
Both children update from one change, so the input shows "ke" and the list shrinks at the same time. Neither child was told about the other, and both are computed from the same lifted query, which is the whole payoff.
The empty query matches everything, since "".includes is true for every string. That falls out of the filter rather than needing a special case, and it is the reason an unfiltered list needs no extra branch.
onQueryChange doing an assignment followed by a re-render is exactly what setQuery does inside React. Seeing it written out makes clear that the child is not modifying anything, it is asking the owner to modify and the owner deciding to re-render.
Note that resultsList never touches query state and never calls onQueryChange, because it only reads. Only the component that needs to change the value receives the function prop, which keeps the direction of control obvious from the props alone.
Where cart state lives
For a CartIcon in the header and a CartPage in the main area, the cart lives in the closest component that contains both, likely App, passed down to each as props.
The procedure is always the same: find the closest common ancestor of every consumer. A header and a main area usually meet at App, so App owns the cart in state and passes it down.
Closest matters as much as common. Hoisting state higher than necessary works and makes every render between the owner and the consumers happen for no reason, so the right target is the lowest component that contains all of them.
Duplicating the count in both components and syncing them with effects recreates the two-sources-of-truth bug React exists to prevent. The icon and the page would drift apart under any update path that forgot one of them, which is lesson 1-1 reappearing inside a React app.
Note that a cart at App level is also the point where people reach for Context or a store, because passing it through several intermediate layers gets tedious. That is a plumbing improvement on top of this rule, and the ownership answer does not change.
How a child requests a change
The parent passes a function prop, meaning a callback, for example onAdd={item => setItems([...items, item])}.
The child calls onAdd(newItem), and the parent, who owns the state, performs the update. The child cannot write to props, and it can call something, and that asymmetry is the entire mechanism.
The direction summarizes cleanly: data flows down as values, and changes flow up as function calls. You already saw it as onQueryChange={setQuery}, where the passed function was the setter itself.
The naming convention is worth adopting because it makes the direction readable at the call site. Props starting with on are things the child calls, and everything else is data the child reads, so scanning a component's props tells you what it can do as well as what it knows.
Note that the parent decides what the change means, not the child. onAdd could append, deduplicate, cap the list at ten, or reject the item entirely, and the child's code is identical in every case, which is what keeps children reusable.