State & Events
Props let data flow into a component, but components also need memory of their own—a value that changes over time and triggers a re-render when it does. That memory is state, and you manage it with the useState Hook.
The useState Hook
useState returns a pair: the current value and a setter function. Calling the setter schedules a re-render with the new value.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
The argument to useState is the initial value, used only on the first render. After that, React preserves the value across renders.
When the next state depends on the previous one, pass an updater function:
setCount(c => c + 1). This is correct even when multiple updates batch together in one event.
State is immutable—replace, don’t mutate
React detects changes by comparing references. If you mutate an object or array in place, the reference doesn’t change and React may skip the re-render. Always create a new object or array:
const [user, setUser] = useState({ name: "Ada", theme: "dark" });
// wrong — mutates existing object
user.theme = "light";
setUser(user);
// right — new object with the spread operator
setUser({ ...user, theme: "light" });
The same rule applies to arrays—use map, filter, and the spread operator instead of push, splice, or index assignment:
setTodos([...todos, newTodo]); // add
setTodos(todos.filter((t) => t.id !== id)); // remove
setTodos(todos.map((t) => t.id === id ? { ...t, done: true } : t)); // update
Handling events
You attach handlers with camelCased props and pass a function reference, not a function call:
function Form() {
function handleSubmit(e) {
e.preventDefault(); // stop the full-page reload
console.log("submitted");
}
return <form onSubmit={handleSubmit}>...</form>;
}
React wraps native events in a cross-browser SyntheticEvent with the familiar preventDefault() and stopPropagation() methods.
Controlled inputs
A controlled input has its value driven by state: React is the single source of truth. You read from state via value and write back via onChange.
function SearchBox() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search…"
/>
);
}
Because state holds the current value, you can validate, transform, or reset it at any moment. (An uncontrolled input keeps its value in the DOM and is read via a ref—useful for simple forms and file inputs.)
Lifting state up
When two components need to share or react to the same data, you lift the state up to their nearest common parent and pass it down as props plus a callback.
function TemperatureApp() {
const [celsius, setCelsius] = useState(20);
return (
<>
<TempInput value={celsius} onChange={setCelsius} />
<p>{(celsius * 9) / 5 + 32}°F</p>
</>
);
}
function TempInput({ value, onChange }) {
return (
<input
type="number"
value={value}
onChange={(e) => onChange(Number(e.target.value))}
/>
);
}
State lives in the parent; both children stay in sync because they read from one source. This is the canonical React pattern for keeping sibling components consistent.
Best Practices
- Keep state minimal—derive values during render instead of storing what you can compute.
- Use the functional updater form when the next state depends on the previous.
- Never mutate state; always produce a new object or array.
- Prefer controlled inputs so state stays the single source of truth.
- Lift shared state to the closest common ancestor; don’t duplicate it across siblings.
- Co-locate state as low in the tree as possible to limit re-render scope.