useEffect

Synchronizes a component with an external system after render, with an optional cleanup on teardown.

Signature
useEffect(setup: () => (void | (() => void)), dependencies?: ReadonlyArray<any>): void
  • Added in React 16.8.0

Parameters

ParameterTypeDescription
$setup() => void | (() => void)The function with your Effect's logic. It may optionally return a cleanup function. React runs setup after the component commits to the DOM. After every commit with changed dependencies, React runs the cleanup function with the old values first, then setup with the new values. After the component is removed from the DOM, React runs cleanup one final time.
$dependenciesoptionalReadonlyArray<any> | undefinedThe list of all reactive values referenced inside the setup code — props, state, and every variable and function declared directly inside the component body. The list must have a constant number of items and be written inline, like [dep1, dep2, dep3]. React compares each dependency with its previous value using Object.is. If you omit this argument, the Effect re-runs after every commit of the component.

Return value

undefinedReturns undefined.

Errors

React throws an "Invalid hook call" error if you call it outside the top level of a component or custom Hook, and warns in development if the dependency array changes size between renders.

Changelog

  • PHP 16.8.0Introduced with Hooks.
  • PHP 18.0.0In development Strict Mode, React runs one extra setup + cleanup cycle before the first real setup, to stress-test that cleanup undoes setup.
  • PHP 18.0.0Effects triggered by a discrete interaction such as a click may run before the browser paints, so the event system can observe their result. Effects not caused by an interaction generally run after paint.

useEffect runs your setup function after React has committed the render to the DOM, then re-runs it whenever a value in the dependency array changes by Object.is. Reach for it when something outside React has to be kept in sync with your component: a WebSocket or subscription, a window event listener, an IntersectionObserver, a third-party map or chart widget, the document title.

When not to reach for it

Most Effects people write are not synchronization. Data derived from props or state belongs in the render body (or useMemo), and work that happens because a user did something belongs in the event handler that handled it. An Effect that only exists to copy one piece of state into another causes an extra render pass and a stale intermediate frame.

What trips people up

  • Cleanup runs with the old values, before setup runs with the new ones. Write cleanup so it exactly undoes setup — disconnect what you connected, remove what you added.
  • Objects, arrays and functions created in the component body are new on every render, so listing them as dependencies re-runs the Effect every render. Move them inside the Effect, or outside the component.
  • An empty array is not "run once" in development Strict Mode: React deliberately runs setup, cleanup, setup. Correct cleanup makes that invisible.
  • Effects never run during server rendering.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Open a chat or WebSocket connection for the current room and close it when the room changes or the component unmounts.
  • Add a window resize, scroll or keydown listener and remove it in cleanup.
  • Set document.title from component state and restore it on teardown.
  • Initialize a non-React widget (a map, an editor, a chart) against a ref and destroy it in cleanup.

Frequently asked questions

Why does my useEffect run twice on mount?

Development Strict Mode intentionally runs setup, then cleanup, then setup again to prove your cleanup undoes your setup. It does not happen in production builds, but the same setup/cleanup/setup sequence does happen in production whenever a component is unmounted and remounted, so the fix is to write correct cleanup rather than to guard the Effect with a ref.

How do I run an effect only once, on mount?

Pass an empty dependency array: useEffect(() => { … }, []). Note that this only makes sense if the setup genuinely references no props or state; if the linter asks for a dependency, adding it is almost always the correct fix, and if the value is something you want to read but not react to, extract it into a useEffectEvent instead of removing it from the array.

Why is my useEffect causing an infinite loop?

The Effect sets state that is also, directly or indirectly, one of its dependencies — or the dependency array was omitted entirely, which re-runs the Effect after every commit. Check for objects, arrays and inline functions created in the component body, since those are new references on every render and will never compare equal with Object.is.

More examples

Subscribing to a browser event with cleanup

The Effect subscribes once after mount and unsubscribes on unmount, so the listener count never grows. The output is a paragraph whose number updates as the window is resized. Because the empty dependency array means the setup closes over nothing reactive, the listener is added exactly once per mount — and in development Strict Mode you can verify the cleanup is correct, since a leaked listener would show up as duplicate work.

Example · jsx
import { useState, useEffect } from "react";

function useWindowWidth() {
  const [width, setWidth] = useState(() => window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }
    window.addEventListener("resize", handleResize);
    // Cleanup mirrors setup exactly: one add, one remove.
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}

export default function WidthReadout() {
  const width = useWindowWidth();
  return <p>Viewport width: {width}px</p>;
}

Ignoring a stale response after the dependency changes

Switching users quickly starts two requests. Without the `ignore` flag, whichever response arrives last wins, so a slow request for user 1 can overwrite the already-rendered user 2. The cleanup flips `ignore` for the abandoned Effect, so its `.then` callback becomes a no-op and the rendered name always matches the current `userId`. This is the canonical race-condition fix and the reason `useEffect` cleanup exists at all for async work.

Example · jsx
import { useState, useEffect } from "react";

function fetchUser(userId) {
  // Stand-in for a network call.
  return Promise.resolve({ id: userId, name: "User " + userId });
}

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let ignore = false;
    setUser(null);
    fetchUser(userId).then((result) => {
      if (!ignore) {
        setUser(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [userId]);

  if (user === null) {
    return <p>Loading user {userId}…</p>;
  }
  return <p>{user.name} (id {user.id})</p>;
}

export default function ProfileSwitcher() {
  const [userId, setUserId] = useState(1);
  return (
    <div>
      <button onClick={() => setUserId(1)}>User 1</button>
      <button onClick={() => setUserId(2)}>User 2</button>
      <UserProfile userId={userId} />
    </div>
  );
}

The three dependency-array shapes

Clicking the `b` button logs only "every render": the mount-only Effect has already run and the `[a]` Effect sees an unchanged dependency. Clicking `a` logs both "every render" and "a changed to …". The omitted array is not the same as an empty one — omitting it re-runs the Effect on every commit, which is the most common cause of an infinite loop when the Effect also sets state.

Example · jsx
import { useState, useEffect } from "react";

export default function DependencyShapes() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  // 1. No array: runs after EVERY commit of this component.
  useEffect(() => {
    console.log("every render", a, b);
  });

  // 2. Empty array: runs after mount, cleanup on unmount.
  useEffect(() => {
    console.log("mount only");
    return () => console.log("unmount only");
  }, []);

  // 3. Listed deps: re-runs only when `a` changes by Object.is.
  useEffect(() => {
    console.log("a changed to", a);
  }, [a]);

  return (
    <div>
      <button onClick={() => setA(a + 1)}>a = {a}</button>
      <button onClick={() => setB(b + 1)}>b = {b}</button>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.