Skip to content

Intermediate · Building the frontend · topic 15 of 34 · 30 min

State: what the screen remembers

Explanation

State is data a component remembers that can change while the app runs, such as the search text or the list of notes. When state changes, React draws the screen again. Keep each piece of state in one place (the nearest common parent of the components that need it), compute everything else from it, and never change state in place: always make new arrays and objects.

State and props

Props come from the parent and are read-only. State belongs to the component itself and changes over time, usually because the user did something. When state changes, React calls the component again (a re-render) and updates the screen to match.

PropsState
Who owns it?The parentThe component itself
Can the component change it?NoYes, with its setter function
Notes app examplenote passed to NoteCardthe search text, the notes list

useState

useState is a hook (a special React function whose name starts with use) that gives a component one piece of state. It returns the current value and a setter. Calling the setter with a new value tells React to re-render.

javascript
const [query, setQuery] = useState('');        // starts empty
setQuery('physics');                            // re-render with the new value

const [notes, setNotes] = useState([]);
setNotes([newNote, ...notes]);                  // a NEW array: React sees the change
useState returns [value, setter]. Always pass a new value to the setter.
Careful: Never change state in place, for example notes.push(newNote) or note.pinned = true. React compares the old and new values; if you pass the same array back, it thinks nothing changed and the screen does not update. Make copies with [...array], array.map, array.filter and { ...object }.

Lifting state up

SearchBar needs the search text to show it in the input. NoteList needs the same text to know which notes to show. If each kept its own copy, they would disagree. The fix is lifting state up: move the state to the nearest common parent (NotesPage) and pass it down as props, together with a function to change it.

Think of it like this: In a gully cricket match, one person keeps the score on a slate, and everyone reads the same slate. If both teams kept their own score, there would be a fight by the tenth over. Lifting state up means keeping one slate in the place everyone can see: the nearest common parent.
javascript
function NotesPage({ initialNotes }) {
  const [notes, dispatch] = useReducer(notesReducer, initialNotes);
  const [query, setQuery] = useState('');

  // derived: computed on every render, never stored in its own state
  const visible = sortForDisplay(filterNotes(notes, query));

  return (
    <main>
      <SearchBar value={query} onChange={setQuery} />
      <NoteList
        notes={visible}
        onDelete={(id) => dispatch({ type: 'deleted', id })}
        onTogglePin={(id) => dispatch({ type: 'pinToggled', id })}
      />
    </main>
  );
}
NotesPage owns the state. SearchBar and NoteList only get props.

Derived values: compute them, do not store them

A derived value is anything you can work out from state you already have. The visible list is notes filtered by query, so compute it during render. Do not add a third piece of state called filteredNotes: you would have to remember to update it every time notes or query change, and one day you will forget.

Tip: Before adding state, ask: "Can I calculate this from props or existing state?" If yes, calculate it. Counting pinned notes, the filtered list and "is the form valid?" are all derived.

useReducer for the notes list

The notes list changes in many ways: add, edit, delete, pin. useReducer puts all those changes in one pure function called a reducer. It takes the current state and an action (an object such as { type: 'deleted', id: 4 }) and returns the next state. Components call dispatch(action) instead of setting state directly. Because the reducer is a plain function, you can test it without React.

javascript
function notesReducer(notes, action) {
  switch (action.type) {
    case 'deleted':
      return notes.filter((n) => n.id !== action.id);
    case 'pinToggled':
      return notes.map((n) => (n.id === action.id ? { ...n, pinned: !n.pinned } : n));
    // 'added' and 'updated' come in the task
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}
A reducer never changes the old array; it returns a new one.

UI state and server state

UI state lives only in the browser: the search text, whether a dialog is open, a half-typed form. Server state is data the server owns, like the notes in the database. The browser only holds a copy, which can go out of date when you edit a note on another device. In the next module you will load notes with fetch and keep that copy fresh with a library called TanStack Query. For now, the reducer holds the notes so we can build the screens.

Watch the lesson

State: what the screen remembers · 1/7

State = memory change -> re-render

State is what a component remembers, like the search text. When it changes, React re-renders.

Prefer a video by a teacher? Find free videos on “react useState lifting state up useReducer tutorial” on YouTube ↗

In real life: A shopping cart in an online store

In an online store, the cart items are state. The header shows a cart badge with the item count, and the cart page shows the full list, so the cart state lives high up in the app (the nearest common parent of both) and is passed down. The badge number and the total price are not stored separately; they are calculated from the items every time. Adding an item creates a new list instead of changing the old one, so every part of the screen updates together.

Practice (0/3 done)

Task 1 · Code · Javascript

Write filterNotes(notes, query), the derived value for the search box. Trim the query; if it is empty, return a copy of all notes. Otherwise return the notes whose title OR body contains the query, ignoring upper and lower case, in their original order. body may be missing.

Task 2 · Code · Javascript

Write notesReducer(notes, action) for useReducer. It must never change the old array or its objects; always return a new array. Actions: { type: "added", note } puts the note FIRST; { type: "updated", note } replaces the fields of the note with the same id ({ ...old, ...note }); { type: "deleted", id } removes that note; { type: "pinToggled", id } flips pinned for that note. Any other type throws an Error("Unknown action: " + type).

Task 3 · Practice

Plan the state of the Notes app screens. Draw the component tree for the notes page and the edit page, then write where each piece of state lives and which values are derived (computed). Use these candidates: the notes list, the search text, the visible list, the number of pinned notes, the form's title and body, the form errors, the saving flag, whether the delete confirmation dialog is open.

Quick quiz

Question 1

1.SearchBar shows the search text and NoteList filters by it. Where should the search text state live?

Question 2

2.Why does notes.push(newNote); setNotes(notes); fail to update the screen?

Question 3

3.Which value should be computed during render instead of stored in state?

Question 4

4.What does a reducer do?

Question 5

5.Which of these is server state?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Forms and client-side validation