State and Lifecycle
State is memory that triggers a new render, and every render is a snapshot
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
You call setCount(count + 1) three times in one click handler, starting at 0. What is count after the next render?
Real-world analogy
What is it?
State is data that a component remembers between renders. When you change it through its setter, React renders the component again. Each render is a snapshot: the function runs with one fixed set of values and produces the UI for them. To change what is on screen, you do not edit the page. You set new state, and React runs the function again and applies the difference.
Real-world relevance
A todo list holds an array in state. Adding a task builds a new array with the new item at the end. Toggling one builds a new array where one object is copied with a flipped flag. The "items left" counter is computed from the array on each render, not stored. Teams that mutate the array in place hit the classic bug where the data changed but the screen did not.
Key points
- State is memory for a component — A plain variable resets on every render. A value from useState is kept by React between renders, and setting it asks React to render again.
- useState gives value and setter — Write const [count, setCount] = useState(0). TypeScript infers the type from the start value, and you add one for unions such as useState<User | null>(null).
- A render is a snapshot — Inside one render, the state variable holds a single value. Calling setCount(count + 1) three times adds one, because all three calls read the same snapshot.
- Use functional updates — Write setCount(c => c + 1) when the new value depends on the old one. React passes the latest queued value, so repeated calls add up correctly.
- Updates are batched — React groups several setter calls from one event into one render. The variable in your current function does not change until the next render.
- Do not mutate state — React decides to re-render by comparing references. Make a new array or object with spread, map or filter instead of calling push on the old one.
- Mount, update, unmount — A component mounts when it first appears, updates on each render, and unmounts when removed. State lives from mount to unmount, and is reset if the component is removed.
- Lift state up — When two siblings need the same value, move the state to their closest common parent. Pass the value down and a setter or callback down with it.
- Derive, do not store — If a value can be computed from state, compute it during render. Storing a copy such as a count of finished todos means two values that can drift apart.
Quick check
Answer from memory — scrolling up first makes it stick less.
Which line updates a todo list correctly?
Two sibling components need the same value. Where should the state live?
Code example
import { useState } from 'react';
type Todo = { id: string; text: string; done: boolean };
export function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [text, setText] = useState('');
const add = () => {
if (!text.trim()) return;
setTodos(prev => [...prev, { id: crypto.randomUUID(), text, done: false }]);
setText('');
};
const toggle = (id: string) =>
setTodos(prev => prev.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
const remaining = todos.filter(t => !t.done).length;
return (
<div>
<input aria-label="New todo" value={text} onChange={e => setText(e.target.value)} />
<button type="button" onClick={add}>Add</button>
<p>{remaining} left</p>
<ul>
{todos.map(t => (
<li key={t.id}>
<label>
<input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} />
{t.text}
</label>
</li>
))}
</ul>
</div>
);
}Line-by-line walkthrough
- 1. Two pieces of state, both typed: todos is an array of Todo objects, and text is the string in the input.
- 2. add builds a new array with spread and appends the new item. The functional form uses the latest list.
- 3. toggle uses map to return a new array. The matching todo is copied with a flipped done flag, and the others are reused as they are.
- 4. remaining is derived from todos on each render. It is not stored, so it cannot drift from the list.
- 5. The input is controlled: its value comes from state, and onChange writes back to state.
- 6. Each list item has key={t.id}, and the checkbox calls toggle with that id.
Spot the bug
// assume: import { useState } from 'react';
function Cart() {
const [items, setItems] = useState<string[]>([]);
function addItem(name: string) {
items.push(name);
setItems(items);
}
return <button onClick={() => addItem('tea')}>Add ({items.length})</button>;
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- State: A Component's Memory (React)
- Updating Arrays in State (React)
- Free Full-Stack Playbook (React and TypeScript lessons) (Teamz Lab)
Lock it in