← React for Beginners

Sharing state between components

Lesson 09 of 12 · 10 min

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.

Before: SearchBox holds the query and NoteList cannot reach it, because data never travels sideways between siblings. After: App holds the query and passes it down to both children. Broken App SearchBox query NoteList needs query siblings cannot see each other Lifted App query SearchBox gets query NoteList gets notes one owner, two readers
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.