useInsertionEffect

Injects DOM elements such as style rules before any layout effects fire, for CSS-in-JS library authors.

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

Parameters

ParameterTypeDescription
$setup() => void | (() => void)The function with your Effect's logic. It may optionally return a cleanup function. When your component is added to the DOM, but before any layout Effects fire, React runs your setup function. After every re-render with changed dependencies, React first runs the cleanup function with the old values, then runs setup with the new values. When your component is removed from the DOM, React runs the cleanup function.
$dependenciesoptionalReadonlyArray<any> | undefinedThe list of all reactive values referenced inside the setup code — props, state, and every variable and function declared directly inside your 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 don't specify the dependencies at all, the Effect re-runs after every re-render of the component.

Return value

undefinedReturns undefined.

Errors

React errors if you schedule a state update from inside useInsertionEffect; it is also an "Invalid hook call" if called outside the top level of a component or custom Hook.

Changelog

  • PHP 18.0.0Introduced so CSS-in-JS libraries could inject rules without forcing a style recalculation inside every layout Effect.

useInsertionEffect is the earliest of the three Effect timings: React runs it when the component is added to the DOM but before any layout Effect fires. That ordering is the whole point. If a CSS-in-JS library injects a rule during a layout Effect, every other layout Effect that already measured a node has measured it against the wrong styles, and the browser is forced into an extra style recalculation mid-commit.

When to reach for it

Essentially never, unless you are writing a CSS-in-JS runtime and need a place to insert <style> tags or call sheet.insertRule. Application code that wants to run before paint should use useLayoutEffect; anything else should use useEffect. Note also that injecting styles at runtime is slower than generating CSS at build time or using static classes, so this Hook exists for libraries that have already committed to the runtime approach.

What trips people up

  • You cannot update state from inside it.
  • Refs are not attached yet, so you cannot read or measure a node.
  • It may run either before or after React has updated the DOM — do not assume the tree is in any particular state.
  • Cleanup and setup interleave one component at a time, rather than all cleanups running before all setups as with the other Effect Hooks.

Example

Example · jsx
import { useInsertionEffect } from "react";

// A minimal CSS-in-JS runtime. Rules are inserted before any layout Effect
// runs, so a component that measures a node in useLayoutEffect already sees
// the final styles.
let styleSheet = null;
const inserted = new Set();

function getSheet() {
  if (styleSheet === null) {
    const tag = document.createElement("style");
    document.head.appendChild(tag);
    styleSheet = tag.sheet;
  }
  return styleSheet;
}

function useCSS(rule) {
  useInsertionEffect(() => {
    if (!inserted.has(rule)) {
      inserted.add(rule);
      getSheet().insertRule(rule, getSheet().cssRules.length);
    }
  }, [rule]);

  // Return the class name the caller should apply.
  return rule.slice(1, rule.indexOf(" "));
}

function Button({ label }) {
  const className = useCSS(".dynamic-button { padding: 8px 16px; border-radius: 4px; }");
  return <button className={className}>{label}</button>;
}

export default function Toolbar() {
  return (
    <div>
      <Button label="Save" />
      <Button label="Cancel" />
    </div>
  );
}

When to use it

  • Insert a generated CSS rule into a shared stylesheet before any component measures a node against it.
  • Register and reference-count a style tag so it is removed when the last component using it unmounts.
  • Prime a CSS-in-JS cache during hydration without triggering a mid-commit style recalculation.

Frequently asked questions

When should I use useInsertionEffect instead of useLayoutEffect?

Only when you are authoring a CSS-in-JS library and need to insert style rules before any component measures the DOM. Application code has no reason to reach for it: it cannot set state, refs are not attached when it runs, and it gives no guarantee about whether the DOM has been updated, so useLayoutEffect or useEffect is the right choice for anything else.

Why can't I set state in useInsertionEffect?

It runs before React has finished committing, so scheduling a re-render from it would mean re-entering a commit that is still in progress. React reports an error if you try. If you need to react to something you discovered during insertion, store it outside React and read it with useSyncExternalStore, or move the logic to useLayoutEffect.

Does useInsertionEffect run on the server?

No — like all Effects, it only runs on the client, so server-rendered HTML contains none of the styles it would inject. CSS-in-JS libraries handle this by collecting rules during server rendering through their own mechanism and emitting them into the HTML, then using useInsertionEffect for rules created after hydration.

More examples

Reference-counted rules with cleanup

Three badges share two distinct rules. The counter means the teal rule is inserted once and survives until both teal badges unmount, which is what stops a shared rule disappearing out from under a still-mounted component. The interleaving caveat matters here: unlike the other Effect Hooks, React runs cleanup and setup one component at a time, so a rule can be decremented to zero and immediately re-incremented in the same commit — the count, not the order, is what keeps the sheet correct.

Example · jsx
import { useInsertionEffect } from "react";

const rules = new Map(); // rule text -> { index, count }
let sheet = null;

function getSheet() {
  if (sheet === null) {
    const tag = document.createElement("style");
    tag.dataset.source = "mini-css";
    document.head.appendChild(tag);
    sheet = tag.sheet;
  }
  return sheet;
}

function useRule(rule) {
  useInsertionEffect(() => {
    const entry = rules.get(rule);
    if (entry === undefined) {
      const index = getSheet().cssRules.length;
      getSheet().insertRule(rule, index);
      rules.set(rule, { index, count: 1 });
    } else {
      entry.count += 1;
    }

    return () => {
      const current = rules.get(rule);
      current.count -= 1;
      if (current.count === 0) {
        rules.delete(rule);
        // A real library would also fix up the stored indices here.
        getSheet().deleteRule(current.index);
      }
    };
  }, [rule]);
}

function Badge({ color, children }) {
  useRule(".badge-" + color + " { color: " + color + "; }");
  return <span className={"badge-" + color}>{children}</span>;
}

export default function BadgeRow() {
  return (
    <p>
      <Badge color="teal">shipped</Badge> <Badge color="teal">shipped</Badge>{" "}
      <Badge color="crimson">failed</Badge>
    </p>
  );
}

Why not useLayoutEffect for style injection

The ordering guarantee is the reason the Hook exists. Insertion Effects for the whole commit run first, then layout Effects, then passive Effects. Because of that, the measured `offsetHeight` includes the 24px of padding the rule adds. If `useCSS` used `useLayoutEffect` instead, the measurement in `StyledPanel` could run before or after the injection depending on tree order, giving an unpadded height on some renders and forcing the browser to recalculate styles partway through the commit.

Example · jsx
import { useRef, useLayoutEffect, useInsertionEffect } from "react";

function useCSS(rule) {
  // Correct: the rule is in the sheet before ANY layout Effect measures.
  useInsertionEffect(() => {
    document.head.appendChild(
      Object.assign(document.createElement("style"), { textContent: rule })
    );
  }, [rule]);
}

function StyledPanel() {
  useCSS(".panel { padding: 24px; }");

  const ref = useRef(null);
  useLayoutEffect(() => {
    // Reads the padded height, because insertion Effects have already run.
    console.log("height", ref.current.offsetHeight);
  }, []);

  return (
    <div ref={ref} className="panel">
      Measured after styles are in the sheet.
    </div>
  );
}

export default function App() {
  return <StyledPanel />;
}

Discussion

  • Be the first to comment on this lesson.