Claude Fable 5.1 & GPT-6 Astra packages are live

Hooks

Free · MIT

React hooks — the rules that make them work, dependency correctness, custom hook design, and the ones that replace an effect entirely.

244 lines8.8 KB Claude Frontend
Target models
Claude Fable 5.1Claude Opus 5Claude Sonnet 5Claude 5 FamilyFuture Claude Models
Name
hooks
Category
Frontend
Description
React hooks — the rules that make them work, dependency correctness, custom hook design, and the ones that replace an effect entirely.
License
MIT
Author
Agent.md maintainers
Last verified
2026-08-23
Reviewed by
unreviewed

FORBIDDEN: Truncating code or writing placeholders such as "// ... existing code ..." or "# rest unchanged". Every edit is complete and applies as written. FORBIDDEN: Reporting a check as passed without showing the command and its output. REQUIRED: Reason through the rules below before the first edit; when two rules conflict, the one stated first wins.

  • Never silence the lint rule to stop a loop. It converts a visible re-render problem into an invisible stale-data problem.

#Purpose

Rules for using and writing hooks. Hooks are positional: React identifies them by call order, not by name. Everything in the Rules of Hooks follows from that one implementation detail.

Component-level state modelling is Frontend/react.


#The rules, and why they exist

tsx
// Broken — the hook order changes between renders, so React associates
// state with the wrong hook and the component corrupts silently.
if (isLoggedIn) { const [name, setName] = useState(""); }

// Correct — unconditional call, conditional value
const [name, setName] = useState("");
  • Call hooks at the top level only. Never inside a condition, loop, nested function, or after an early return.
  • Call them only from components or other hooks.
  • Enable eslint-plugin-react-hooks and treat both rules-of-hooks and exhaustive-deps as errors. A disabled exhaustive-deps warning is a stale closure waiting to happen.

An early return before a hook is the most common accidental violation — it makes the hook count differ between renders.


#Dependencies are not a suggestion

tsx
// Stale closure: `query` is captured from the first render forever
useEffect(() => { search(query); }, []);

// Correct
useEffect(() => { search(query); }, [query]);

If the exhaustive list causes a loop, the fix is upstream, not a shortened array:

SymptomReal causeFix
Effect loops on an object/array depNew reference each renderuseMemo it, or depend on a primitive field
Effect loops on a function depNew function each renderuseCallback, or move it inside the effect
Effect needs a value but should not re-run on itA latest-value read, not a dependencyuseEffectEvent, or a ref
Effect re-runs on every renderMissing dependency array entirelyAdd one
tsx
// Depend on the field, not the object identity
useEffect(() => { track(user.id); }, [user.id]);

Never silence the lint rule to stop a loop. It converts a visible re-render problem into an invisible stale-data problem.


#The right hook for the job

HookUse for
useStateIndependent values
useReducerSeveral values that change together, or complex transitions
useRefA mutable value that must not trigger a render; DOM handles
useMemoA genuinely expensive computation, or a stable reference
useCallbackA stable function identity passed to a memoised child
useSyncExternalStoreReading any store outside React
useIdGenerated ids that must match between server and client
useDeferredValueKeeping input responsive while an expensive list updates
useTransitionMarking a state update as non-urgent
useOptimisticShowing the result of a mutation before it confirms

useSyncExternalStore is the one people miss. Reading localStorage, matchMedia, navigator.onLine or an external store with useState + useEffect produces a hydration mismatch and a flash of wrong content:

tsx
const isOnline = useSyncExternalStore(
  (cb) => { addEventListener("online", cb); addEventListener("offline", cb);
            return () => { removeEventListener("online", cb); removeEventListener("offline", cb); }; },
  () => navigator.onLine,        // client snapshot
  () => true                     // server snapshot — must be deterministic
);

A ref changing does not re-render. If the UI must reflect a value, it is state, not a ref.


#Effects need cleanup

tsx
useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal })
    .then((r) => r.json())
    .then(setData)
    .catch((e) => { if (e.name !== "AbortError") setError(e); });
  return () => controller.abort();        // cancels on unmount AND on url change
}, [url]);

Without the cleanup, two rapid changes to url can resolve out of order and the stale response wins — the classic React data race.

Every subscription, timer, listener and observer must be torn down. Strict Mode mounts, unmounts and remounts each component in development specifically to expose a missing cleanup: if that breaks your component, it is already broken.

For data fetching, prefer a library (@tanstack/react-query, SWR) or a framework loader. They handle caching, deduplication, retries and races — all of which you would otherwise reimplement per component.


#Custom hooks

Extract a custom hook when stateful logic is reused. Extract a plain function when the logic has no state — a function is easier to test and to reason about.

tsx
// Named for what it does, returns a stable shape, cleans up after itself
export function useDebouncedValue<T>(value: T, delayMs = 300): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(id);
  }, [value, delayMs]);
  return debounced;
}
  • Name it useX — the lint rules depend on that prefix to apply hook rules.
  • Return a consistent shape: a tuple for two values, an object for more.
  • Do not accept props wholesale; take the specific values needed.
  • A custom hook containing no hooks should be a plain function.
  • Test with renderHook from @testing-library/react.

#Anti-patterns

Anti-patternWhy it failsFix
Conditional or early-return hook callsHook order shifts; state corruptsUnconditional top-level calls
Silencing exhaustive-depsStale closures read old valuesFix the real dependency
Empty deps with captured valuesValue frozen at first renderList every dependency
Object or array as a dependencyNew reference each render; infinite loopDepend on primitive fields
Effect without cleanupLeaks; out-of-order responsesReturn a teardown
Fetch in an effect without abortRace conditions; stale data winsAbortController or a data library
Ref used for rendered dataChanging it does not re-renderUse state
useState + useEffect for an external storeHydration mismatch; flash of wrong contentuseSyncExternalStore
Many useState for one conceptImpossible states; scattered updatesuseReducer
useCallback/useMemo everywhereCost without measured benefitProfile first
Custom hook not prefixed useLint rules do not applyName it useX
Custom hook taking whole propsRe-runs on unrelated changesTake specific values
Stateless helper written as a hookHarder to test for no reasonPlain function
Breaking on Strict Mode double-invokeThe bug is real, not the modeMake effects idempotent

#Checklist

  • Hooks are called unconditionally at the top level of every component
  • No hook follows an early return
  • eslint-plugin-react-hooks runs with both rules set to error
  • No exhaustive-deps suppression exists without a written justification
  • Dependency arrays are complete; object identity is not depended on
  • Every effect returns a cleanup for its subscriptions, timers and requests
  • Data fetching aborts or ignores stale responses
  • Components survive Strict Mode double-invocation
  • External stores are read with useSyncExternalStore
  • Refs hold only values that must not trigger a render
  • Related state uses useReducer rather than many booleans
  • Memoisation is applied only where profiling justified it
  • Custom hooks are prefixed use and return a consistent shape
  • Stateless logic is a plain function, not a hook
  • Custom hooks are tested with renderHook