Can you explain the most important React hooks you use day to day?
The five hooks that cover 95% of component code: useState (local reactive state), useEffect (subscriptions/side-effects with cleanup), useRef (mutable values that survive re-renders + DOM access), useMemo (cache an expensive value across renders), useCallback (cache a function identity for memoized children).
Five hooks every React engineer should know cold.
1. useState — local component state
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
setCount(c => c + 1); // functional updater — safe under batching- Returns
[value, setter]. - Setter triggers a re-render.
- Pass an initializer function (
useState(() => heavy())) for one-time expensive init.
2. useEffect — side effects with cleanup
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup
}, []);- Runs after commit, not during render.
- Cleanup runs before next effect AND on unmount.
- Empty deps = mount/unmount only. Missing deps = stale closures.
3. useRef — mutable value, no re-render
const inputRef = useRef<HTMLInputElement>(null);
const renderCount = useRef(0);
renderCount.current++; // mutating .current does NOT trigger re-renderTwo uses: DOM access (<input ref={inputRef} />) and instance variables that need to survive re-renders without causing them.
4. useMemo — cache a computed value
const sorted = useMemo(
() => items.slice().sort(expensiveCompare),
[items],
);Skip the recomputation if deps haven't changed. Only worth it for genuinely expensive work or to keep referential equality for downstream memoization.
5. useCallback — cache a function identity
const onClick = useCallback(() => doThing(id), [id]);Sugar for useMemo(() => fn, deps). Only useful when the function is passed to a React.memo'd child or used in another hook's deps — otherwise it's overhead.
When NOT to reach for them
useMemo/useCallbackeverywhere: profile first, the bookkeeping itself isn't free.useStatefor derived values: compute in render instead —const fullName = first + ' ' + last.useEffectfor state-from-props: use akeyto reset, or compute in render.
Beyond the five
useContext, useReducer, useTransition, useId, useSyncExternalStore, useDeferredValue round out the modern toolkit.
Follow-up questions
- •When would you choose useReducer over useState?
- •What's the difference between useMemo and useCallback?
- •How do you avoid stale closures in useEffect?
Common mistakes
- •Reading a ref's .current during render to drive UI — it doesn't trigger re-renders.
- •Adding useCallback/useMemo without a downstream memoized consumer — pure overhead.
- •Missing deps in useEffect, then suppressing the lint warning instead of fixing it.
Performance considerations
- •Hook calls themselves are cheap. The cost comes from re-renders triggered by useState and the closures captured by useEffect/useCallback. Profile before scattering useMemo — the dependency check + extra closure can cost more than the recomputation you're avoiding.
Edge cases
- •StrictMode in dev double-invokes effects — cleanup must be idempotent.
- •useState's initializer runs once even though the arg expression evaluates every render — wrap heavy init in a function.
- •useRef returns the same object across renders, but .current is mutable and not reactive.
Real-world examples
- •A form: useState for fields, useEffect to debounce a save, useRef for the imperative focus() on submit-fail, useMemo to sort a typeahead suggestion list, useCallback to pass a stable onChange to memoized inputs.