Effect Hooks

Effect includes dealing with network, browser DOM, animations, widgets written using a different UI library, and other non-React code.


  • useEffect connects a component to an external system.
  • useLayoutEffect fires before the browser repaints the screen. You can measure layout here.
  • useInsertionEffect fires before React makes changes to the DOM. Libraries can insert dynamic CSS here.

useEffect

Parameters

  • setup: The function with your Effect’s logic. Your setup function may also optionally return a cleanup function. When your component is added to the DOM, React will run your setup function. After every re-render with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. After your component is removed from the DOM, React will run your cleanup function.

  • optional dependencies: The list of all reactive values referenced inside of the setup code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. If you ignore this argument, your Effect will re-run after every re-render of the component.

Caveats

  • If you’re not trying to synchronize with some external system, you probably don’t need an Effect.

  • When Strict Mode is on, React will run one extra development-only setup+cleanup cycle before the first real setup. This is a stress-test that ensures that your cleanup logic “mirrors” your setup logic and that it stops or undoes whatever the setup is doing.

  • If your Effect wasn’t caused by an interaction (like a click), React will generally let the browser paint the updated screen first before running your Effect. If your Effect is doing something visual (for example, positioning a tooltip), and the delay is noticeable (for example, it flickers), replace useEffect with useLayoutEffect.

External systemes

  • A timer managed with setInterval() and clearInterval().

  • An event subscription using window.addEventListener() and window.removeEventListener().

  • A third-party animation library with an API like animation.start() and animation.reset().

If you’re not connecting to any external system, you probably don’t need an Effect.

import { useEffect } from "react";
import { createConnection } from "./chat.js";

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState("https://localhost:1234");

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}

React calls your setup and cleanup functions whenever it’s necessary, which may happen multiple times:

  1. Your setup code runs when your component is added to the page (mounts).

  2. After every re-render of your component where the dependencies have changed:

  • First, your cleanup code runs with the old props and state.

  • Then, your setup code runs with the new props and state.

  1. Your cleanup code runs one final time after your component is removed from the page (unmounts).

Let’s illustrate this sequence for the example above.

When the ChatRoom component above gets added to the page, it will connect to the chat room with the initial serverUrl and roomId. If either serverUrl or roomId change as a result of a re-render (say, if the user picks a different chat room in a dropdown), your Effect will disconnect from the previous room, and connect to the next one. When the ChatRoom component is removed from the page, your Effect will disconnect one last time.

Example: Wrapping Effects in custom Hooks

If you find yourself often needing to manually write Effects, it’s usually a sign that you need to extract some custom Hooks for common behaviors your components rely on.

function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId,
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState("https://localhost:1234");

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
  });
  // ...
}

If you have an existing codebase, you might have some Effects that suppress the linter like this:

useEffect(() => {
  // ...
  // Avoid suppressing the linter like this:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

When dependencies don’t match the code, there is a high risk of introducing bugs. By suppressing the linter, you “lie” to React about the values your Effect depends on. Instead, prove they’re unnecessary.

You Might Not Need an Effect

useLayoutEffect

useLayoutEffect is a version of useEffect that fires before the browser repaints the screen.

useEffect does not block the browser from repainting but useLayoutEffect blocks.

The code inside useLayoutEffect and all state updates scheduled from it, prevent the browser from repainting the screen. When used excessively, this makes your app slow. When possible, prefer useEffect.

import { useState, useRef, useLayoutEffect } from "react";

function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);
  // ...
}

Example: Measuring layout before the browser repaints the screen

Most components don’t need to know their position and size on the screen to decide what to render. They only return some JSX. Then the browser calculates their layout (position and size) and repaints the screen.

Sometimes, that’s not enough. Imagine a tooltip that appears next to some element on hover. If there’s enough space, the tooltip should appear above the element, but if it doesn’t fit, it should appear below. In order to render the tooltip at the right final position, you need to know its height (i.e. whether it fits at the top).

To do this, you need to render in two passes:

  1. Render the tooltip anywhere (even with a wrong position).

  2. Measure its height and decide where to place the tooltip.

  3. Render the tooltip again in the correct place.

All of this needs to happen before the browser repaints the screen. You don’t want the user to see the tooltip moving. Call useLayoutEffect to perform the layout measurements before the browser repaints the screen:

function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0); // You don't know real height yet

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height); // Re-render now that you know the real height
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // It doesn't fit above, so place below.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

export function TooltipContainer({ children, x, y, contentRef }) {
  return (
    <div
      style={{
        position: "absolute",
        pointerEvents: "none",
        left: 0,
        top: 0,
        transform: `translate3d(${x}px, ${y}px, 0)`,
      }}
    >
      <div ref={contentRef} className="tooltip">
        {children}
      </div>
    </div>
  );
}

Here’s how this works step by step:

  1. Tooltip renders with the initial tooltipHeight = 0 (so the tooltip may be wrongly positioned).

  2. React places it in the DOM and runs the code in useLayoutEffect.

  3. Your useLayoutEffect measures the height of the tooltip content and triggers an immediate re-render.

  4. Tooltip renders again with the real tooltipHeight (so the tooltip is correctly positioned).

  5. React updates it in the DOM, and the browser finally displays the tooltip.

useInsertionEffect

useInsertionEffect allows inserting elements into the DOM before any layout effects fire.

useInsertionEffect is for CSS-in-JS library authors.

Caveats

  • You can’t update state from inside useInsertionEffect.

  • By the time useInsertionEffect runs, refs are not attached yet.

  • useInsertionEffect may run either before or after the DOM has been updated. You shouldn’t rely on the DOM being updated at any particular time.

  • Unlike other types of Effects, which fire cleanup for every Effect and then setup for every Effect, useInsertionEffect will fire both cleanup and setup one component at a time. This results in an “interleaving” of the cleanup and setup functions.

Example: Injecting dynamic styles from CSS-in-JS libraries

If you prefer CSS-in-JS you will have three options:

  1. Static extraction to CSS files with a compiler

  2. Inline styles, e.g. <div style={{ opacity: 1 }}>

  3. Runtime injection of <style> tags

If you use CSS-in-JS, we recommend a combination of the first two approaches (CSS files for static styles, inline styles for dynamic styles).

It is not recommended to inject <styles> tag because:

  1. Runtime injection forces the browser to recalculate the styles a lot more often.

  2. Runtime injection can be very slow if it happens at the wrong time in the React lifecycle.

The first problem is not solvable, but useInsertionEffect helps you solve the second problem.

Call useInsertionEffect to insert the styles before any layout effects fire:

// Inside your CSS-in-JS library
let isInserted = new Set();
let collectedRulesSet = new Set();

function useCSS(rule) {
  // useInsertionEffect does not run on the server
  // If you need to collect which CSS rules have been used on the server, you can do it during rendering:
  if (typeof window === "undefined") {
    collectedRulesSet.add(rule);
  }
  useInsertionEffect(() => {
    // As explained earlier, we don't recommend runtime injection of <style> tags.
    // But if you have to do it, then it's important to do in useInsertionEffect.
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
}

function Button() {
  const className = useCSS("...");
  return <div className={className} />;
}

React Lifecycle

Each component in React has a lifecycle which you can monitor and manipulate during its three main phases.

The three phases are: Mounting, Updating, and Unmounting.

  • Mounting means putting elements into the DOM.

  • Updating is when a component is updated.

  • Unmounting is when a component is removed from the DOM.