Intermediate · Building the frontend · topic 15 of 34 · 30 min
State: what the screen remembers
Explanation
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.
| Props | State | |
|---|---|---|
| Who owns it? | The parent | The component itself |
| Can the component change it? | No | Yes, with its setter function |
| Notes app example | note passed to NoteCard | the 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.
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 changeLifting 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.
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>
);
}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.
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.
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);
}
}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 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)
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.
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).
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
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.