Hooks
7 pages · ~5 min✓ Reviewed
Builds on State and Props. Next up: Effects and Data Fetching.
Hooks Mental Model and Rules
1. What a hook is
- A hook is a function whose name starts with
use - Lets a function component keep state and effects across renders
- React matches hooks to state by call order, not by name
2. The two rules
javascript
const [n, setN] = useState(0); useEffect(() => { if (on) save(n); }, [on, n]);
put the condition inside the hook, never around it
3. Each render is a snapshot
- Renderprops, state fixed
- Eventhandler runs
- setStateschedules render
- New rendernew values
- Inside one render,
countnever changes - A state update does not change it; it schedules a new render
Remember
Same hooks, same order, every render. That order is how React finds your state.
useState and useEffect
1. useState
useState(0): initial value used first render onlyuseState(() => expensive()): lazy initializer- Updates are batched; value changes on the next render
- Depends on previous value? Use a functional update
- State is replaced, not merged: copy with spread, never mutate
javascript
setCount(c => c + 1);
setUser({ ...user, name });new reference, so React sees the change
2. useEffect
| Deps | Runs |
|---|---|
| none | every render |
[] | once on mount |
[a, b] | when a or b changes |
- Runs after paint
- Return a cleanup: unsubscribe, clear timers, abort fetches
- Cleanup runs before the next effect and on unmount
javascript
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);useRef, useMemo and useCallback
1. useRef: a box that never re-renders
useRef(init)returns a stable{ current }object- Changing
ref.currentdoes not trigger a re-render - Use it for DOM nodes and mutable values: timer IDs, the previous value
jsx
const inputRef = useRef(null);
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>
Focus
</button>ref={inputRef} fills current with the DOM node
2. useMemo vs useCallback
| Hook | Caches | Recomputes when |
|---|---|---|
useMemo(() => compute(a, b), [a, b]) | a value | a or b changes |
useCallback(fn, deps) | a function identity | deps change |
useCallback(fn, deps)equalsuseMemo(() => fn, deps)- Useful with
React.memochildren - Useful as an effect dependency
3. When to memoize
Hint, not a guarantee
Memoization is a performance hint. Add it only after measuring a real cost or an identity problem.
useReducer, useContext and Custom Hooks
1. useReducer
const [state, dispatch] = useReducer(reducer, init)- Prefer it over useState for related fields or many transitions
dispatch({ type: 'add', payload })is the only way to change state- Reducer is pure: return a new object, never mutate
javascript
function reducer(state, action) { switch (action.type) { case 'add': return { ...state, items: [...state.items, action.payload] }; default: return state; } }
2. useContext
useContext(Ctx)reads the nearest<Ctx.Provider value>- Value changes → every consumer re-renders
- So memoize the value object
javascript
const value = useMemo(() => ({ user, setUser }), [user]); return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
3. Custom Hooks
- Function named useX that calls other hooks:
useFetch,useLocalStorage - Each call gets its own independent state
| Custom hook | Context | |
|---|---|---|
| Shares | logic | state |
| Two components | separate values | same value |
Logic, not state
Two components calling useFetch do not share values. To share them, put the state in context
Common Hook Mistakes
1. Stale and Looping Effects
- Stale closure: missing deps read old state; list the deps or use a functional update
- Infinite loop:
setStatein an effect with no deps, or an object/array literal in deps - Updater form
setN(n => n + 1)needs nonin deps
javascript
useEffect(() => {
const id = setInterval(() => setN(n => n + 1), 1000);
return () => clearInterval(id);
}, []);no stale n, no dep needed
2. Identity and Mutation
| Mistake | Effect | Fix |
|---|---|---|
Literal {}, [] or () => {} in deps | new each render; breaks useMemo, useCallback, React.memo | memoize it |
arr.push(x) or obj.x = 1, then setState(same) | same reference, no re-render | copy: [...arr, x] |
3. Fetch Races
- No cleanup: a late response overwrites newer state
- Use an ignore flag or AbortController
javascript
useEffect(() => {
let ignore = false;
fetch(url).then(r => r.json())
.then(d => { if (!ignore) setData(d); });
return () => { ignore = true; };
}, [url]);Remember
Fresh deps, fresh copies, cleanup every effect.
React Hooks Cheat Sheet
1. State and effects
| Call | Does |
|---|---|
useState(init) | [v, setV]; use setV(prev => next) when it depends on the previous value |
useEffect(fn) | runs every render |
useEffect(fn, []) | runs on mount only |
useEffect(fn, [x]) | runs when x changes |
- Return a cleanup function from the effect
- It runs before the next effect and on unmount
2. Refs, caches, reducers
| Hook | Remember |
|---|---|
useRef | mutable box, no re-render; DOM nodes, persistent values |
useMemo | caches a value |
useCallback | caches a function |
useReducer | dispatch(action) into a pure reducer |
useContext | reads the nearest Provider value |
3. Rules
- Call hooks at the top level only: no if, loops, nested functions
- Call them only in components or custom hooks
- Custom hooks are named
useX; each call has its own state
Remember
React matches hooks by call order, so keep that order identical on every render.
Check yourself
With n = 0, one click runs setN(n + 1); setN(n + 1);. What is n on the next render?
- 1, not 2: both calls read the same snapshot, n = 0
setN(c => c + 1)twice gives 2
useEffect(() => { setInterval(() => setC(c + 1), 1000) }, []) starts with c = 0. What does c show after 5 seconds?
- 1, and it stays there: the closure captured c = 0 on mount
- Fix:
setC(x => x + 1), and return a cleanup that clears the interval
items.push(x); setItems(items) shows no change on screen. Why?
- Same reference, so React sees no change and skips the re-render
- Fix:
setItems([...items, x])