---
title: "06 — The Core Hooks — useState, useReducer, useEffect, and the Ref/Memo Family"
uid: core-hooks
tags: ["usereducer", "useeffect", "useref", "react", "usestate", "roadmap:react", "usememo", "usecallback", "hooks"]
excerpt: "Hooks are primitives, each with one job: useState/useReducer hold state, useEffect syncs with the outside world, and useRef/useMemo/useCallback escape or optimize when those aren't enough."
date: 2026-08-13T03:27:45+0000
source: https://www.aveshina.my.id/en/blog/core-hooks
---

An endless API list was how the hooks family felt until I matched each one to its job. The framing that consolidated it: **hooks are primitives, each with one job.** [1] useState and useReducer hold state. useEffect synchronizes with the outside world. useRef, useMemo, and useCallback are tools for the cases where those two aren't enough — escaping the render cycle, caching expensive work, and stabilizing function identity. Once I matched each hook to its job, the list stopped feeling arbitrary.

## The Rules of Hooks

Before the individual hooks, the two rules that govern all of them [2]:

- **Call hooks at the top level only** — never inside loops, conditions, or nested functions. React relies on call order to match each hook to its state, so conditional hooks break that accounting.
- **Call hooks from React functions** — a component, or a custom hook. Not from a regular utility.

These rules are why hooks exist at all — they let React track per-component state without me passing around instances. Every lint failure pointing at a conditional hook is React protecting me from corrupted state.

## useState: the basic state primitive

useState adds local state to a function component [3]. It returns the current value and a setter; calling the setter schedules a re-render with the new value. Three details that I had to internalize:

- **The setter is the only way to update.** Direct mutation does nothing visible.
- **Use the callback form when the next value depends on the previous** (setCount(c => c + 1)) — updates are batched and may be async.
- **State is per-instance and persists across renders**, but resets when the component unmounts (or its position in the tree changes — see the notes on reconciliation).

```
const [open, setOpen] = useState(false);
```

useState is the default for independent, simple values. When state gets tangled — multiple fields that update together, or a value whose next state depends on a complex previous state — that's the signal to reach for useReducer.

## useReducer: state with transitions

useReducer is the alternative to useState for state that follows clear transitions [4]. I provide a reducer (state, action) => newState and a dispatch function; components call dispatch({ type: 'increment' }) rather than setCount(c => c + 1). The state logic lives in one testable function instead of scattered across handlers.

```
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'reset':     return { count: 0 };
    default: return state;
  }
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
```

If you've used Redux, this is exactly that pattern, locally. The signal to switch from useState to useReducer: when one user action needs to update several related pieces of state at once, or when the next-state logic is more than a one-liner. A form with many interdependent fields, a multi-step wizard, a game's turn state — these all read more clearly as reducers.

## useEffect: synchronizing with the outside world

useEffect runs side effects after render — fetching data, subscribing, reading from localStorage, integrating a non-React library [5]. The model is "synchronize with an external system": the effect sets things up, the cleanup tears them down, and React re-runs the pair whenever the dependencies change. The notes on the lifecycle cover this in depth; the key points for these notes:

- Always provide an explicit dependency array.
- Always pair setup with cleanup.
- If the effect body doesn't touch anything outside React, you probably don't need it — derive state during render instead.

## useRef: values that survive renders without triggering them

useRef returns a mutable object whose .current persists across renders [6]. Changing .current does **not** trigger a re-render, which is exactly the point. Two jobs:

- **Reaching into the DOM** — <input ref={inputRef} /> gives me the actual node for imperative focus, measurement, or third-party libraries.
- **Storing a value that shouldn't cause renders** — a debounce timer id, an "is mounted" flag, a cached value that React shouldn't watch.

```
const timerRef = useRef(null);
timerRef.current = setTimeout(...); // changing this never re-renders
```

The rule: if the UI should change when the value changes, it's state, not a ref.

## useMemo and useCallback: caching for a reason

useMemo caches the result of a computation; useCallback caches a function definition [7][8]. They are not free — every memoization adds bookkeeping, and the deps comparison itself costs something. They earn their keep in specific situations:

- **Expensive computation.** A calculation that takes meaningful time and runs on every render — memoize it.
- **Referential equality for props.** React compares props by reference — is it the *same* object, not just equal contents. If I pass an object, array, or function as a prop to a memoized child (React.memo), the child still re-renders unless that prop's identity is stable. useMemo/useCallback stabilize it.

```
const sorted = useMemo(() => heavySort(items), [items]);   // cache the result
const onSelect = useCallback(id => setActive(id), []);      // stable function identity
```

The framing I keep coming back to (from Kent C. Dodds and Josh Comeau): **don't memoize defensively.** [7][8] Profile first. Most renders are cheap, and premature useMemo adds overhead for no gain. Reach for it when a profiler run shows real cost.

## useContext: data that doesn't fit the prop chain

useContext reads a value from a React Context provider without prop-drilling — threading the value through every component in between [9]. The model: a Provider at the top of the tree declares a value, and any descendant can useContext(MyContext) to read it directly. Theme, locale, the current authenticated user — these are the canonical cases.

```
const theme = useContext(ThemeContext);
```

Two caveats I learned the hard way:

- **Every provider value change re-renders every consumer**, even ones that only read part of the value. Splitting contexts (one per slice) or moving to an external store avoids the re-render storm on large trees.
- **Context is not a state management library.** It's a dependency-injection mechanism — a way to hand a value down to any component that asks, without passing it through the layers in between. For frequently-changing application state, a dedicated store (Zustand, Jotai) is the better tool — covered in the notes on state management.

## Custom hooks: extracting reusable logic

When the same hook combination shows up in several components, I extract it into a **custom hook** — a plain function whose name starts with use [10]. Custom hooks are how hooks replace the old HOC and render-prop patterns: logic moves into a function I can call from any component.

```
function useWindowSize() {
  const [size, setSize] = useState({ w: 0, h: 0 });
  useEffect(() => {
    const onResize = () => setSize({ w: innerWidth, h: innerHeight });
    window.addEventListener('resize', onResize);
    onResize();
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return size;
}
```

The two Rules of Hooks apply here too — a custom hook can call other hooks, but only at its top level.

```figure
<svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="The hooks family mapped to jobs. Left column: useState and useReducer both feed into 'state'. Middle: useEffect feeds 'sync with outside world'. Right column split into useRef (escape render), useMemo (cache value), useCallback (cache function identity), useContext (skip prop-drilling). A banner across the top says 'Rules of Hooks: top-level only, from React functions'.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- rules banner -->
    <rect x="40" y="16" width="680" height="34" rx="8" fill="#1f2937" stroke="#374151" stroke-width="1.5"/>
    <text x="380" y="38" font-size="11.5" font-weight="700" fill="#f9fafb" text-anchor="middle">Rules of Hooks — top-level only, and only from React functions</text>

    <!-- Group A: state -->
    <rect x="40" y="80" width="200" height="100" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="140" y="100" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">state</text>
    <text x="140" y="124" font-size="11" font-family="ui-monospace,monospace" fill="#422006" text-anchor="middle">useState</text>
    <text x="140" y="144" font-size="11" font-family="ui-monospace,monospace" fill="#422006" text-anchor="middle">useReducer</text>
    <text x="140" y="166" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">independent values vs transitions</text>

    <!-- Group B: effects -->
    <rect x="270" y="80" width="220" height="100" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="380" y="100" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">sync with outside world</text>
    <text x="380" y="130" font-size="11" font-family="ui-monospace,monospace" fill="#052e16" text-anchor="middle">useEffect</text>
    <text x="380" y="166" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">setup + cleanup, runs after render</text>

    <!-- Group C: escape/optimize -->
    <rect x="520" y="80" width="200" height="200" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="620" y="100" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">escape / optimize</text>
    <text x="620" y="126" font-size="11" font-family="ui-monospace,monospace" fill="#1e1b4b" text-anchor="middle">useRef</text>
    <text x="620" y="158" font-size="11" font-family="ui-monospace,monospace" fill="#1e1b4b" text-anchor="middle">useMemo</text>
    <text x="620" y="190" font-size="11" font-family="ui-monospace,monospace" fill="#1e1b4b" text-anchor="middle">useCallback</text>
    <text x="620" y="222" font-size="11" font-family="ui-monospace,monospace" fill="#1e1b4b" text-anchor="middle">useContext</text>
    <text x="620" y="262" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">no re-render · cache value</text>
    <text x="620" y="276" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">cache fn · skip prop-drill</text>

    <!-- custom hook callout -->
    <rect x="40" y="210" width="450" height="70" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="265" y="232" font-size="12" font-weight="700" fill="#500724" text-anchor="middle">custom hooks — function starting with 'use'</text>
    <text x="265" y="252" font-size="10" fill="#500724" text-anchor="middle">extract reusable combinations of the primitives above</text>
    <text x="265" y="270" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">the modern replacement for HOCs and render props</text>
  </g>
</svg>
```

## How I use this

My default ladder for any new component is to reach for the simplest hook that fits and only escalate when there's a concrete reason. useState for simple values; useReducer the moment state updates get interdependent. useEffect only for genuine external synchronization, always with explicit deps and cleanup. useRef for DOM access and render-immune values. useMemo/useCallback only after a profiler run shows a cost — never defensively. useContext for genuinely global, slowly-changing values (theme, locale, auth session), and a real state store once the data changes often enough that Context's re-renders hurt. And whenever the same hook pattern repeats across components, it becomes a custom hook — that's where the real leverage of the hooks model shows up.

## References

[1] React team, "Hooks reference," react.dev, 2024. [Online]. Available: [https://react.dev/reference/react](https://react.dev/reference/react)

[2] React team, "Rules of Hooks," react.dev, 2024. [Online]. Available: [https://react.dev/reference/rules/rules-of-hooks](https://react.dev/reference/rules/rules-of-hooks)

[3] React team, "useState," react.dev, 2024. [Online]. Available: [https://react.dev/reference/react/useState](https://react.dev/reference/react/useState)

[4] React team, "useReducer," react.dev, 2024. [Online]. Available: [https://react.dev/reference/react/useReducer](https://react.dev/reference/react/useReducer)

[5] React team, "useEffect," react.dev, 2024. [Online]. Available: [https://react.dev/reference/react/useEffect](https://react.dev/reference/react/useEffect)

[6] React team, "useRef," react.dev, 2024. [Online]. Available: [https://react.dev/reference/react/useRef](https://react.dev/reference/react/useRef)

[7] J. Comeau, "useMemo and useCallback," joshwcomeau.com, 2023. [Online]. Available: [https://www.joshwcomeau.com/react/usememo-and-usecallback/](https://www.joshwcomeau.com/react/usememo-and-usecallback/)

[8] K. C. Dodds, "useMemo and useCallback," kentcdodds.com, 2021. [Online]. Available: [https://kentcdodds.com/blog/usememo-and-usecallback](https://kentcdodds.com/blog/usememo-and-usecallback)

[9] React team, "useContext," react.dev, 2024. [Online]. Available: [https://react.dev/reference/react/useContext](https://react.dev/reference/react/useContext)

[10] React team, "Reusing logic with custom hooks," react.dev, 2024. [Online]. Available: [https://react.dev/learn/reusing-logic-with-custom-hooks](https://react.dev/learn/reusing-logic-with-custom-hooks)

```quiz
Q: When does useState vs useReducer become the right call?
- useState for independent simple values; useReducer when state updates are interdependent or follow clear transitions
- always use useReducer, it's newer
- never use useReducer, it's deprecated
correct: 0
explain: useState is the default for simple independent values. useReducer shines when one action updates several related fields, or when next-state logic is complex — the logic lives in one testable reducer.

Q: Changing the .current of a useRef…
- triggers a re-render
- does NOT trigger a re-render — refs live outside the render loop
- throws an error
correct: 1
explain: Refs persist across renders but changing them never schedules a render. That's the point — for DOM nodes and render-immune values. Use state if the UI should change.

Q: useMemo and useCallback should be used…
- defensively, on every component, for performance
- after a profiler shows real cost — most renders are cheap and memoization adds overhead
- only in class components
correct: 1
explain: Memoization isn't free (bookkeeping + deps comparison). Profile first; reach for it for genuinely expensive computation or to stabilize prop identity for memoized children.

Q: A context provider's value changes. What happens?
- only the specific consumers that use the changed field re-render
- every consumer of that context re-renders, even ones reading other fields
- nothing — context never causes re-renders
correct: 1
explain: Context consumers re-render when the provider value changes, regardless of which field they read. Splitting contexts or using a store avoids the re-render storm for frequently-changing data.

Q: You see the same useState + useEffect combination in three components. The idiomatic move is…
- copy-paste it again
- extract a custom hook (function starting with 'use')
- convert the components to classes
correct: 1
explain: Custom hooks are the modern replacement for HOCs and render props — extract the shared hook combination into a named function and call it from each component.
```
