React

React useEffect Hook👨‍💻

useEffect is how you synchronize a React component with something outside of React: a browser API, a third-party library, a network connection. It is also the most misused hook in the ecosystem. Most of the useEffect code in production apps should not be useEffect at all -- it should be an event handler, a useMemo call, or server-side logic that never reaches the client.

Key Takeaways

  • 1useEffect synchronizes your component with an external system (DOM APIs, WebSockets, browser events, third-party libraries). If there is no external system involved, you probably do not need it.
  • 2The cleanup function is not optional for effects that allocate resources. Skipping cleanup causes memory leaks, stale subscriptions, and race conditions that only surface in production.
  • 3The dependency array is a contract, not a performance optimization. Every reactive value referenced inside the effect must be listed. Lying to React about dependencies causes bugs that are painful to debug.
  • 4React 19 guidance: prefer event handlers for user-triggered work, useMemo for derived computations, and server actions for mutations. useEffect is the last resort, not the first tool.
  • 5In Strict Mode (development), React intentionally runs every effect twice (mount, unmount, mount) to surface missing cleanup. If your effect breaks under this pattern, the cleanup is wrong.
  • 6Effects run after the browser paints, not during render. This means they never block the visual update, but it also means you cannot use them to calculate layout synchronously -- use useLayoutEffect for that.

Master react useeffect hook

Take the React Basics course with hands-on lessons and challenges.

Examples

Data fetching with AbortController cleanup

tsx

AbortController cancels the in-flight request when userId changes or the component unmounts. Without this, a slow response for the old userId could overwrite the new user's data -- a classic race condition that only shows up when the network is slow.

WebSocket connection with reconnection on prop change

tsx

When the user switches from AAPL to GOOGL, the cleanup closes the old WebSocket before opening a new one. This prevents receiving stale price updates for the wrong symbol. The status state gives the UI a way to show connection state without a separate loading boolean.

IntersectionObserver for lazy-loading sections

tsx

IntersectionObserver is a browser API -- exactly the kind of external system useEffect is designed for. The observer is disconnected on cleanup to prevent callbacks firing after unmount. Once the section is visible, we unobserve immediately since there is no reason to keep watching.

Document title synchronization

tsx

The cleanup restores the previous title when the component unmounts or when the title value changes. Extracting this into a custom hook keeps the component clean and makes the pattern reusable across pages. This is a textbook useEffect use case: syncing React state with a browser API that React does not control.

Keyboard shortcut listener with cleanup

tsx

Global keyboard listeners must attach to the document and clean up on unmount. The callback is stabilized with useCallback to avoid removing and re-adding the listener on every render. This pattern is how command palettes (Cmd+K) and keyboard shortcuts work in tools like Linear, Notion, and VS Code.

When NOT to use useEffect -- common anti-patterns

tsx

The first anti-pattern filters a list inside useEffect, which causes two renders (one with stale data, one with filtered data). useMemo computes the filtered list during the same render with zero extra cycles. The second anti-pattern resets state via useEffect when a prop changes. A key prop tells React to unmount and remount the component, which resets all state cleanly without any effect.

Common Mistakes

Mistake:

Using useEffect to derive state from props or other state -- e.g. `useEffect(() => setFullName(first + ' ' + last), [first, last])`

Fix:

Compute derived values during render: `const fullName = first + ' ' + last`. If the computation is expensive, wrap it in useMemo. useEffect causes a wasted render cycle because it runs after the component has already painted with stale data.

Mistake:

Missing the dependency array entirely, causing the effect to fire after every single render and often creating infinite update loops

Fix:

Always provide a dependency array. Use `[]` for mount-only effects and `[dep1, dep2]` when the effect depends on reactive values. An effect with no array is almost always a bug.

Mistake:

Putting objects or functions created during render into the dependency array, which triggers the effect on every render because references change

Fix:

Move object or function creation inside the effect body, or stabilize them with useMemo/useCallback. If the effect only uses specific properties of an object, destructure them and depend on the primitives instead.

Mistake:

Ignoring the exhaustive-deps ESLint rule by adding eslint-disable comments instead of fixing the dependency list

Fix:

The rule exists because React cannot track what your effect reads. If the rule flags a missing dependency, either add it, move the value inside the effect, or restructure the code so the dependency is not needed. Suppressing the warning leads to stale closures that silently serve wrong data.

Mistake:

Fetching data in useEffect without cancellation, causing race conditions when dependencies change rapidly (e.g., user typing in a search box)

Fix:

Always use AbortController for fetch requests. When the dependency changes, the cleanup function aborts the previous request so only the latest response is applied. Without this, a slow response for 'rea' can overwrite the results for 'react'.

Best Practices

  • Ask yourself: is there an external system involved? If the answer is no -- if you are just transforming data, resetting state, or responding to a user action -- you do not need useEffect. Use useMemo, event handlers, or a key prop instead.
  • Every effect that allocates a resource (listener, timer, connection, subscription) must return a cleanup function. Test this by enabling Strict Mode -- if your effect breaks on double-mount, the cleanup is missing or incomplete.
  • Keep effects focused on a single concern. An effect that fetches data AND sets up a WebSocket AND registers an event listener should be split into three separate useEffect calls, each with its own cleanup and dependency array.
  • Use AbortController for every fetch inside useEffect. It is the standard Web API for request cancellation and prevents race conditions, stale data, and state updates on unmounted components.
  • Extract repeated effect patterns into custom hooks (useDocumentTitle, useIntersectionObserver, useKeyboardShortcut). This hides the setup/cleanup boilerplate and makes the calling component declarative.
  • Never suppress the react-hooks/exhaustive-deps ESLint rule. If it flags a dependency you think should not be there, the fix is restructuring your code -- not silencing the linter. The rule catches real bugs.

Summary

useEffect exists for one purpose: synchronizing your component with something outside of React. Browser APIs, WebSocket connections, event listeners, third-party libraries -- these are legitimate use cases. If you are computing derived data, resetting state on prop change, or handling user actions, there is a better tool (useMemo, key props, event handlers). Always provide a dependency array, always clean up resources, and never suppress the exhaustive-deps rule. The best useEffect is the one you did not write.

Practice React with hands-on challenges

Learn react useeffect hook hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master React with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.