Claude Fable 5.1 & GPT-6 Astra packages are live

Hooks

Free

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

210 lines8.6 KB Open Ai Frontend
targetModels
GPT-6 AstraGPT-5.6GPT-5.5GPT-5 FamilyFuture GPT 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
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for ChatGPT: scripts/model-profiles.json -->

#Scope contract

FILE_ISOLATION: Modify only files inside the scope the task names. Reading elsewhere is allowed; writing outside it is not, and a needed out-of-scope change is reported, not made. SIGNATURE_PINNING: Before implementing, write the exact signatures you will add or change (name, parameters, return type). Implement to those signatures; if one must change, say so before changing it. TYPE_CONTRACTS: Every public function carries explicit parameter and return types. No any, untyped dict, or interface{} at a module boundary.


#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