React Hooks Pitfalls and Best Practices

Common React Hooks mistakes, stale closure issues, dependency pitfalls, and practical best practices.

View
StandardDetailedCompact
Export
Copy the compact sheet, download it, or print it.
Download
`D` dense toggle · `C` copy all

Rules of Hooks

Avoid invalid hook usage.

Call hooks only at the top level

Do not call hooks in conditions, loops, or nested functions.

tsxANYreacthooksrules-of-hooks
tsx
// ✅
function MyComponent() {
  const [count, setCount] = useState(0);
  // ...
}

// ❌
if (condition) {
  useEffect(() => {});
}
Notes

React relies on consistent hook call order across renders.

Call hooks only from components or custom hooks

Do not call hooks from regular utility functions.

tsxANYreacthooksrules-of-hookscustom-hooks
tsx
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  return isOnline;
}
Notes

A custom hook is just a function whose body legitimately uses other hooks.

Enable eslint-plugin-react-hooks

Catch rules-of-hooks and dependency mistakes early.

bashANYreacthookseslint
bash
npm install -D eslint-plugin-react-hooks
Notes

The hooks ESLint plugin catches hook order problems and effect dependency issues.

Effect Dependency Pitfalls

Common dependency and stale closure issues.

Stale closure pitfall

Effects and callbacks capture values from the render where they were created.

tsxANYreacthooksuseEffectstale-closure
tsx
useEffect(() => {
  const id = setInterval(() => {
    console.log(count); // can go stale if dependencies are wrong
  }, 1000);
  return () => clearInterval(id);
}, [count]);
Notes

If the effect depends on `count`, include it or restructure the code to avoid stale reads.

Memoize objects used in dependencies

Avoid recreating dependency objects every render when identity matters.

tsxANYreacthooksuseMemodependencies
tsx
const options = useMemo(() => ({ roomId, serverUrl }), [roomId, serverUrl]);
useEffect(() => {
  const conn = createConnection(options);
  conn.connect();
  return () => conn.disconnect();
}, [options]);
Notes

Changing object identity retriggers effects even when fields look the same.

Move non-reactive logic out of effects

Keep effects focused on synchronization.

tsxANYreacthooksuseEffectbest-practices
tsx
useEffect(() => {
  subscribe(userId);
  return () => unsubscribe(userId);
}, [userId]);
Notes

Effects are easier to reason about when they do only synchronization and cleanup.

Performance Pitfalls

Avoid over-memoization and unnecessary effects.

Do not memoize everything

Memoization has a cost; use it when it solves a concrete problem.

tsxANYreacthooksuseMemoperformance
tsx
// Not every derived value needs useMemo.
const fullName = `${firstName} ${lastName}`;
Notes

Simple computations are often cheaper and clearer without `useMemo`.

Derive data during render when possible

Avoid effects that only derive data from props/state.

tsxANYreacthooksderived-statebest-practices
tsx
// Prefer deriving directly:
const filtered = items.filter(item => item.active);

// Instead of syncing derived state in an effect.
Notes

If something can be calculated from current props/state, compute it during render rather than syncing another state variable.

Perform event-specific work in event handlers

Do not move user-triggered logic into effects unnecessarily.

tsxANYreacthooksuseEffectbest-practices
tsx
function handleSave() {
  saveDraft(form);
}

<button onClick={handleSave}>Save</button>
Notes

Effects should synchronize with external systems because rendering happened, not because you want to run arbitrary imperative code later.

Recommended next

No recommendations yet.