Lesson 2 of 5
State and events: making components interactive
3 min read
State is data that belongs to a component and changes over time. When state changes, React re-renders that component (and its children) with the new value.
useState in depth
const [value, setValue] = useState(initialValue);Things that trip people up:
1. State updates are not immediate. Calling setCount schedules a re-render; the count variable in the current render does not change.
function handleClick() {
setCount(count + 1);
console.log(count); // still the old value
}2. Use the updater form when the new value depends on the old one.
// Bug: both calls read the same stale `count`, so it only goes up by 1
setCount(count + 1);
setCount(count + 1);
// Correct: each updater receives the latest pending value, so +2
setCount((c) => c + 1);
setCount((c) => c + 1);3. Batching. Since React 18, multiple state updates in the same event, timeout or promise are batched into one re-render.
4. Never mutate state. React compares by reference. If you push into the same array, React sees the same reference and may skip the update.
// Wrong
todos.push(newTodo);
setTodos(todos);
// Right: create a new array / object
setTodos([...todos, newTodo]);
setUser({ ...user, city: "Pune" });
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
setTodos(todos.filter((t) => t.id !== id));5. Lazy initial state. If the initial value is expensive, pass a function: useState(() => JSON.parse(localStorage.getItem("cart") ?? "[]")). It runs only on the first render.
Events
React events are camelCased (onClick, onChange, onSubmit) and receive a synthetic event object that behaves the same in every browser.
<button onClick={handleClick}>Save</button> // pass the function
<button onClick={handleClick()}>Save</button> // bug: calls it during render
<button onClick={() => remove(item.id)}>Delete</button> // pass an argumentControlled forms
In a controlled input, React state is the single source of truth: the input shows value from state and every keystroke updates state.
function SignupForm() {
const [form, setForm] = useState({ email: "", password: "" });
const [error, setError] = useState("");
function update(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}
function submit(e) {
e.preventDefault();
if (form.password.length < 8) return setError("Use at least 8 characters.");
api.signup(form);
}
return (
<form onSubmit={submit}>
<input name="email" type="email" value={form.email} onChange={update} />
<input name="password" type="password" value={form.password} onChange={update} />
{error && <p role="alert">{error}</p>}
<button>Create account</button>
</form>
);
}An uncontrolled input keeps its own value in the DOM and you read it with a ref (useRef) or FormData on submit. Uncontrolled is simpler for big forms that are only read on submit; controlled is better when you need live validation, formatting or dependent fields. Libraries like React Hook Form use uncontrolled inputs for performance.
Where should state live?
- Keep state as local as possible: in the lowest component that needs it.
- When two siblings need the same data, lift state up to their closest common parent and pass it down.
- Don't duplicate state. If something can be computed from props or other state, compute it during render.
// Avoid: fullName kept in sync by hand
const [fullName, setFullName] = useState("");
// Prefer: derive it
const fullName = `${first} ${last}`;
const visible = todos.filter((t) => (filter === "done" ? t.done : true));Check yourself
- Why does
setCount(count + 1)twice only add one? - What is the difference between a controlled and an uncontrolled input?
- What does "lifting state up" mean, and when do you need it?
Sign up free to save your progress.