React Hooks: A Working Guide to the Ones You Need
useState, useEffect, useRef, useReducer and the newer hooks — what each is for, the dependency-array rules, and the mistakes that cause infinite loops.
Table of contents
- useState
- useEffect
- useRef
- useReducer
- The newer ones worth knowing
- The rules of hooks, and why
- Frequently asked questions
- Why does my effect run twice in development?
- Do I still need useMemo and useCallback?
- Can I call hooks conditionally if I am careful?
- How do I share logic between components?
- Related reading
- References
Hooks are functions that let a component hold state and subscribe to external systems. There are about fifteen; five carry almost all the weight.
useState#
const [count, setCount] = useState(0);Two rules that prevent most state bugs:
Use the updater form when the new value depends on the old one.
setCount(count + 1); // stale if called twice in one event
setCount((c) => c + 1); // always correctNever mutate state. React compares by reference, so mutating an object or array produces no re-render:
items.push(newItem);
setItems(items); // no re-render
setItems([...items, newItem]); // correctInitialise expensively with a function. useState(expensiveInit()) runs on every render; useState(() => expensiveInit()) runs once.
useEffect#
useEffect synchronises a component with something outside React — a subscription, a timer, an imperative browser API. It is not a general "run after render" hook, and treating it as one is the source of most useEffect pain.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((error) => {
if (error.name !== 'AbortError') setError(error);
});
// Cleanup runs before the next effect and on unmount. Without it, a fast
// change of `id` lets an older, slower response overwrite a newer one.
return () => controller.abort();
}, [id]);The dependency array is a correctness feature, not a performance one. Omitting a value you read leaves the effect using a stale copy.
// Unnecessary effect
const [full, setFull] = useState('');
useEffect(() => {
setFull(`${first} ${last}`);
}, [first, last]);
// Just derive it
const full = `${first} ${last}`;useRef#
Two distinct jobs:
// 1. A DOM handle
const inputRef = useRef(null);
<input ref={inputRef} />;
inputRef.current.focus();
// 2. A mutable value that does NOT trigger re-render
const renderCount = useRef(0);
renderCount.current++;The second is how you keep a timer id, a previous value, or a "has this already run" flag without causing renders.
useReducer#
Reach for it when several pieces of state change together, or when the next state depends on the action rather than just the previous value:
function reducer(state, action) {
switch (action.type) {
case 'submit':
return { ...state, status: 'loading', error: null };
case 'success':
return { status: 'done', data: action.data, error: null };
case 'failure':
return { ...state, status: 'idle', error: action.error };
default:
return state;
}
}Three useState calls for status, data and error can get into impossible combinations — loading and an error, for instance. A reducer makes each transition explicit and the impossible states unreachable.
The newer ones worth knowing#
useId— generates a stable id that matches between server and client. Use it forhtmlFor/aria-describedby; neverMath.random().useActionState— wires a form to a Server Action with pending and result state, and keeps the form working without JavaScript.useOptimistic— shows an expected result immediately and reconciles when the real one arrives.useSyncExternalStore— the correct way to subscribe to a non-React store; it avoids tearing during concurrent rendering.
The rules of hooks, and why#
Call hooks only at the top level, and only from components or other hooks. React tracks hooks by call order, not by name — so a hook inside an if changes the order between renders and state gets attached to the wrong hook. The ESLint plugin catches this; keep it enabled.
Frequently asked questions#
Why does my effect run twice in development?#
Strict Mode intentionally mounts, unmounts and remounts components to surface missing cleanup. If running twice breaks something, the effect is missing a cleanup function — that is the bug it is designed to reveal.
Do I still need useMemo and useCallback?#
Less than before, and the React Compiler removes most remaining cases. See when they are still worth it.
Can I call hooks conditionally if I am careful?#
No. There is no careful version — the call order must be identical on every render.
How do I share logic between components?#
Write a custom hook. Any function starting with use that calls other hooks is one, and it is the intended mechanism.