Key takeaways
- Hooks must be called at the top level of React components or custom Hooks.
- Static analysis catches structural violations but cannot prove runtime correctness.
- Dependency warnings usually reveal synchronization design, not formatting problems.
- Use the official React ESLint rules in the real project.
React Hooks rely on call order. React associates state and other Hook data with the sequence of calls made while rendering a component. If a Hook is called conditionally, in a loop, or from an ordinary function, that sequence can change and React can associate data with the wrong call. The Rules of Hooks are therefore correctness rules, not style preferences.
The React Hooks Checker can flag common structural patterns in a pasted component. It is best used as a first pass. The official eslint-plugin-react-hooks understands a broader set of React rules and project syntax, and React’s runtime behavior still depends on props, state, effects, network timing, and user actions that static source alone cannot reproduce.
The two foundational rules
Call Hooks only at the top level. Do not put a useState, useEffect, or custom Hook call inside a condition, loop, event handler, nested callback, or after an early return that runs on some renders. Top-level calls let React encounter Hooks in the same order every time the component renders.
Call Hooks only from React functions: function components and custom Hooks. A utility named useSomething is not automatically safe; it must itself follow the rules and be called from React code. Conversely, ordinary functions should not call Hooks merely because they happen to run during rendering.
Modern React guidance also emphasizes purity. Rendering should be idempotent with respect to props, state, and context. Side effects belong in event handlers or Effects that synchronize with external systems, not in the render calculation. Static rules can catch some violations, while Strict Mode helps expose accidental impurities during development.
A conditional Hook failure
function Profile({ user }) {
if (!user) return <p>Loading…</p>;
const [expanded, setExpanded] = useState(false);
return <button onClick={() => setExpanded(!expanded)}>{user.name}</button>;
}
The first render may return before calling useState, while a later render calls it. Move the Hook before the conditional return:
function Profile({ user }) {
const [expanded, setExpanded] = useState(false);
if (!user) return <p>Loading…</p>;
return (
<button onClick={() => setExpanded((value) => !value)}>
{user.name}{expanded ? " (expanded)" : ""}
</button>
);
}
The functional state update also avoids closing over a value when the next state depends on the previous state. This is not required for every update, but it is a robust pattern for toggles and queued updates.
A practical analysis workflow
- Parse as JSX or TypeScript JSX. Plain JavaScript parsing cannot understand every component file. Match the parser to the project extension and syntax.
- Identify React functions. Components are commonly capitalized functions; custom Hooks conventionally start with use. Factories, wrappers, and callbacks can make identification more complex.
- List every Hook call. Include custom Hooks, not only built-ins. Record the enclosing function and control-flow ancestors.
- Check call position. A Hook under an if, switch, loop, try block, callback, or conditional return deserves investigation.
- Review Effect purpose. Ask which external system the Effect synchronizes with. Derived display data often belongs in render rather than an Effect that sets more state.
- Review dependencies. Every reactive value read by the Effect should be represented unless the code is redesigned so the value is no longer reactive.
- Run official lint rules. Use the repository’s plugin version and configuration to catch patterns a lightweight checker cannot model.
- Exercise behavior. Test prop changes, rapid interactions, mount/unmount, failed requests, and development Strict Mode.
Understand dependency warnings
An Effect is a synchronization process. Its dependencies describe which reactive values cause that process to resynchronize. Omitting a value can leave the Effect using a stale closure. Adding an unstable object or function created on every render can cause the Effect to run too often. The solution is not automatically to disable the warning or memoize everything.
First decide whether an Effect is needed. If you are calculating filtered items from props, calculate them during render. If an action happens because the user clicked a button, put it in the event handler. Use an Effect when the component must synchronize with something outside React: a network subscription, browser API, widget, timer, or external store.
When an Effect is appropriate, make its setup and cleanup symmetrical. A subscription should unsubscribe; a timer should be cleared; a request may need cancellation or an ignore flag to prevent stale results from winning a race. Development Strict Mode intentionally runs extra setup and cleanup cycles to reveal missing cleanup.
What static analysis can confidently report
A parser can locate Hook-like calls, enclosing functions, and many control-flow constructs. A linter can reason about lexical references in an Effect and identify likely missing dependencies. It can enforce naming and structural conventions across code without executing the application.
Static analysis cannot know every runtime identity or business guarantee. A function may be stable because of a framework contract unknown to the rule. A custom abstraction may hide an external subscription. Dynamic property access and generated code can reduce certainty. Good diagnostics explain the exact source pattern and leave room for project context.
The official React ESLint plugin now covers the foundational rules-of-hooks and exhaustive-deps checks as well as diagnostics related to React Compiler. React documents that a compiler diagnostic may cause only the affected component to be skipped, allowing teams to address findings incrementally rather than treating every warning as an emergency rewrite.
Common mistakes
- Disabling exhaustive-deps to stop repeated Effects without fixing the stale closure.
- Wrapping every function in useCallback even when no stable identity is required.
- Copying props into state with an Effect and creating two sources of truth.
- Fetching in an Effect without handling stale responses or unmounting.
- Calling a custom Hook from an event handler because its name looks like a utility.
- Mutating props, state, or values already passed to JSX.
- Expecting a static checker to prove that cleanup runs correctly under real timing.
- Testing only the initial render and missing behavior after prop or route changes.
Review checklist
- Every Hook is called unconditionally at the top level of a component or custom Hook.
- Custom Hooks start with use and encapsulate a coherent reusable behavior.
- Rendering contains no side effects or direct mutation of props and state.
- Each Effect has a clear external synchronization target.
- Setup and cleanup are symmetrical.
- Dependency lists reflect the reactive values read by the Effect.
- Event-driven work lives in event handlers where appropriate.
- Official lint rules, Strict Mode, and interaction tests are enabled.
Frequently asked questions
Can I call a Hook inside useMemo or an event callback?
No. The callback is a nested function, so its calls do not participate in the component’s stable top-level Hook order. Call Hooks at the component or custom-Hook top level, then use their returned values inside callbacks.
Is useEffect required for every asynchronous operation?
No. User-initiated work often belongs in the event handler that knows why it happened. Framework data APIs may provide better loading, caching, and server-rendering behavior. Use an Effect when rendering itself requires synchronization with an external system.
Is a missing dependency always safe to add?
Adding it makes the declared synchronization more accurate, but may expose a loop caused by an unstable value or by setting state unnecessarily. Reconsider the Effect’s design, move non-reactive logic, or stabilize only the identity that truly must remain stable.
Why does Strict Mode run my Effect twice in development?
React uses an additional setup and cleanup cycle in development to reveal code that cannot safely restart. Production behavior differs, but the development signal is valuable: make cleanup complete rather than adding a flag that hides the issue.
Further reading
Hooks code becomes easier to reason about when call order is fixed, rendering is pure, and every Effect has one clear synchronization job.
Try the related tool
Apply the guide to a focused example, then verify the result in your own project.
Open the related tool →