hooks-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited hooks-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
React Hooks are the discipline of treating useState, useEffect, useMemo, useCallback, useRef, and their kin not as a generic toolkit but as primitives with precise semantics, all resting on one mechanism: React identifies hooks by call order, matching the first hook call in a render to slot 0 of the component's fiber, the second to slot 1, and so on. Every "rule" of hooks falls out of preserving that order — the Rules of Hooks (top-level only, React-functions only) exist to keep slot indices stable across renders; dependency arrays exist because each callback captures a closure over the render that created it, so a missing or reference-unstable dependency is a stale-closure or over-firing bug; useEffect exists to synchronize with systems React does not own (DOM, network, timers, third-party widgets), not to compute derived state that belongs in render; custom hooks exist to reuse or name stateful logic, not to shorten a long component; and useMemo/useCallback/memo stabilize referential identity only when a memoized consumer or a provably-expensive computation makes that worthwhile. React 18/19 semantics — automatic batching, interruptible concurrent rendering, Strict Mode double-invocation, Effect Events, and the React Compiler's automatic memoization — change the calculus around effects and manual memoization but never the underlying call-order invariant. The skill's posture at every hook call is one question: what invariant am I expressing, and what is the cheapest primitive that expresses it?
The discipline of using React Hooks correctly: why the Rules of Hooks are a call-order invariant rather than a convention, how the dependency array encodes a contract between a closure and the next render, when useEffect is the wrong primitive (and what the right one is), the difference between derived values and stored values, the three legitimate reasons to extract a custom hook, when useMemo, useCallback, and memo actually prevent rerenders and when they merely add overhead, and the React 18/19 semantics that change the calculus: automatic batching, concurrent rendering, Strict Mode effect stress checks, Effect Events, and React Compiler.
A React component is a function from props and state to a description of UI. Hooks are the primitives that let the function remember things across calls without breaking referential transparency from the outside — to the calling renderer, each render is a fresh function call producing fresh output; to the component, useState returns "the same" state across renders.
This illusion is held together by a single mechanism: React identifies hooks by call order. The first useState call in a render is matched to slot 0 of the fiber's hook list, the second to slot 1, and so on. The Rules of Hooks exist to keep that call order stable across renders. A conditional useState would shift the slot indices and corrupt the state of every later hook in the component. The lint rule that flags conditional hooks is not enforcing style; it is preventing a class of memory-corruption bug from compiling.
Once that foundation is internalized, every other "discipline" rule around hooks falls out of it: dependency arrays exist because hooks capture closures over the render's props and state, and a stale closure is a referential bug. useEffect exists for synchronizing with systems outside React, not for general "do this when X changes" logic — most uses are better expressed as derived values during render. Custom hooks exist when stateful logic must be reused across components, not as a stylistic preference for shorter component bodies. useMemo and useCallback exist to stabilize referential identity for downstream React.memo or hook dependency arrays, not as general performance optimizations.
The discipline is to ask, at each hook call, what invariant am I expressing? — and to reach for the cheapest primitive that expresses it. Most stale-closure bugs, most "why does my effect run twice" mysteries, and most over-memoized components come from reaching for hooks as a generic toolkit rather than as primitives with specific semantics.
The two rules:
The mechanism they protect:
// First render
const [a, setA] = useState(0); // slot 0
const [b, setB] = useState(''); // slot 1
useEffect(() => {...}, [a]); // slot 2
// Second render — what if the second hook is conditional?
const [a, setA] = useState(0); // slot 0
if (a > 0) {
const [b, setB] = useState(''); // SOMETIMES slot 1, SOMETIMES skipped
}
useEffect(() => {...}, [a]); // either slot 1 OR slot 2 — slot mismatchReact does not store hooks by name. It stores them by call-order index. A conditional hook makes the index of every later hook depend on the condition. State and effect state get reassigned to wrong slots, and the component starts reading another hook's state — a classic memory-corruption pattern, except in JavaScript with garbage collection it surfaces as silent bugs rather than crashes.
eslint-plugin-react-hooks and the React runtime in development mode both enforce these rules. The cost of violating them is not lint-failure aesthetics; it is undefined behavior.
Every callback you pass to useEffect, useMemo, useCallback, and similar hooks captures the variables in scope at the time of the render that produced it. The dependency array is the contract that says: "the next render's callback should replace this one if any of these values changed; otherwise keep using the cached one."
Three failure modes:
x but x is not in the array. The effect runs with the value of x from the render where the effect was last created — which may be many renders behind reality. Symptom: "the value seems frozen" or "this used to update and now doesn't."Object.is, so a new array literal [a, b] is never === to the previous one. The effect re-runs every render even when the meaningful values didn't change.// eslint-disable-next-line react-hooks/exhaustive-deps. The escape hatch exists, but every use should be a documented exception with a comment explaining why the missing dependency is intentional. Most production stale-closure bugs trace to silent disables.The right fix depends on the case:
useMemo / useCallback at its source, or refactor so the effect depends on a primitive (string, number) rather than a reference.useEffectEvent (RFC stage as of writing) — separate the "reactive" reads from the "latest snapshot" reads. Until that lands, a useRef mirror of the value is the documented escape hatch.useEffect Is the Wrong PrimitiveuseEffect is for synchronizing the component with an external system: a DOM subscription, a network request, a timer, a third-party widget. It is not for "do this calculation when X changes." Most of the patterns below are mis-uses:
| Pattern | What it does | Better primitive |
|---|---|---|
useState(initial); useEffect(() => setState(derive(props)), [props]) | Stores a derived value in state, then re-syncs on prop change | Compute during render: const derived = derive(props); |
useState; useEffect(() => setState(...)) triggered by a prop change to reset | Resets local state when a prop changes | Use the key prop to remount, or lift state up |
useEffect(() => { fetchData().then(setData) }, []) for initial data | Imperative fetch-on-mount | Server Components, React Query / SWR, or Suspense data fetching |
useEffect(() => onChange?.(value), [value]) to notify parent | Re-syncs parent state from child effect | Call onChange in the event handler that changed value |
useEffect(() => { const id = setTimeout(...); return () => clearTimeout(id) }, [...]) to debounce | Effect-driven debounce | Custom useDebouncedValue hook, or library |
The React docs' essay You Might Not Need an Effect is the canonical taxonomy of effect misuse. The pattern: every useEffect is a code smell until it has earned its place by genuinely synchronizing with something React doesn't own.
A value is derived if it can be computed from other values you already have (props, state, refs). A value is stored if it has its own lifecycle — set by an event, persisted across renders, and not recoverable from other inputs.
Stored:
const [draft, setDraft] = useState('');
<input value={draft} onChange={e => setDraft(e.target.value)} />Derived (no extra useState needed):
const fullName = `${firstName} ${lastName}`; // from props
const isValid = email.includes('@') && password.length >= 8; // from state
const filteredItems = items.filter(i => i.tag === activeTag); // from props+stateThe rule: if the value can be computed during render, compute it during render. State exists to remember things across renders; it is not a cache for computation. Storing a derived value in state introduces a two-step update cycle: render → effect → setState → rerender, where a single render with inline computation would have sufficed.
When derived computation is expensive enough to matter, that's what useMemo is for — and only then, because for cheap computations the overhead of useMemo (a deps check, an array allocation, a callback closure) exceeds the cost of re-running the computation.
Three legitimate reasons to extract a custom hook:
useState + useEffect + useCallback whose collective purpose is "track the window's scroll position" deserves to be named useScrollPosition. The name is documentation.useFormDraft is clearer at the call site than the seven hooks it composes.Anti-patterns:
useEffect is almost always wrong — the callback closes over the caller's state, and you've just hidden the dependency-array problem from view.Library precedent: react-use, usehooks-ts, @tanstack/react-query, swr — these libraries are entirely composed of named, reusable custom hooks. They are the existence proof of what custom hooks earn their keep doing.
useMemo and useCallback — Footgun CasesThese hooks have two valid jobs:
React.memo-wrapped child.For everything else, they are net negative — they add a deps-check cost, an allocation, and a closure, in exchange for no observable benefit.
| Case | useMemo adds value? |
|---|---|
const total = items.reduce(...) where items has 5 entries | No |
const total = items.reduce(...) where items has 50,000 entries | Yes (if rerender frequency is high) |
const onClick = useCallback(() => ..., [...]) passed to a normal child | No — the child re-renders anyway when its parent re-renders |
const onClick = useCallback(() => ..., [...]) passed to React.memo(Child) | Yes — keeps the memo equality check from breaking |
const opts = useMemo(() => ({ a, b }), [a, b]) used in another hook's deps | Yes — without it, the outer hook over-fires |
const opts = useMemo(() => ({ a, b }), [a, b]) never used as a dep | No |
The compiler (React Compiler, formerly React Forget) when generally available will auto-memoize where beneficial and eliminate this discipline as a manual concern. Until then, the rule is: don't memoize until you have evidence — a profiler trace, a benchmark, or a React.memo'd consumer — that memoization is preventing real work.
flushSync to force separate updates; conversely, don't write code that depends on updates not batching.Date.now()-driven branching that won't survive replay; (2) effects run after the render commits, not after every render function call.useEffectEvent separates non-reactive reads from an effect's reactive dependencies. It is the right primitive for "read the latest value inside this effect, but do not re-run the effect when that value changes." Do not add Effect Events to dependency arrays.memo for many components and reduces the need for manual useMemo and useCallback. Manual memoization remains useful for expensive computations, stable dependencies, or code not covered by the compiler, but "wrap it just in case" is even weaker guidance now.After applying this skill, verify:
useEffect synchronizes with something React doesn't own; computational logic lives in render or in event handlers.useMemo / useCallback wrap only values that (a) stabilize identity for a downstream consumer or (b) skip provably-expensive work.useEffect misuse.| Instead of this skill | Use | Why |
|---|---|---|
| Choosing between Server Components, Client Components, and where to draw the boundary | client-server-boundary | client-server-boundary owns the serialization contract and the 'use client' / 'use server' rules; hooks-patterns operates on the client side of that boundary. |
| Deciding where the application's state should live (server, client, URL, persistent storage) | state-management | state-management owns the location and ownership decision; hooks-patterns owns the local discipline once you've decided client-component state is the right home. |
| Designing a reusable component library's API surface | component-architecture | component-architecture owns primitive/composite/product layering; hooks-patterns is one component's internal logic. |
| Picking SSR vs SSG vs ISR for a route | rendering-models | rendering-models owns the rendering-strategy decision; hooks-patterns has nothing to say about it. |
| Suspense for data fetching and streaming UI patterns | suspense-patterns | suspense-patterns owns the boundary-and-fallback discipline; hooks-patterns covers the underlying hook primitives but not the Suspense boundary protocol. |
<!-- skill-graph-context:start (generated — do not edit by hand) -->
Classification
frontend-engineeringtrueengineering/frontendWhen to use
React Hooks, is this hook safe to call here, why does my useEffect run twice, dependency array warning, should this be state or derived, extract this into a custom hookNot for
state-management: where state lives across the applicationclient-server-boundary: serialization and use-client/use-server placementrendering-models: SSR, SSG, ISR, streaming, request-time, and client rendering choicessuspense-patterns: boundary and fallback design for async UIRelated skills
code-review, testing-strategy, performance-engineeringrendering-models, client-server-boundary, component-architecture, state-management, suspense-patterns, server-components-design, server-actions-design, performance-engineeringConcept
Keywords
React Hooks, Rules of Hooks, useEffect dependencies, exhaustive deps, useEffectEvent, custom hooks, derived state, stale closure, useMemo useCallback, React Compiler<!-- skill-graph-context:end -->
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.