useInsertionEffect is a version of useEffect that fires before any DOM mutations.
function useInsertionEffect(
effect: () => (() => void) | void,
deps: Array<mixed> | void | null
): void
useInsertionEffect is for CSS-in-JS library authors. Unless you’re working on a CSS-in-JS library and need a place to inject styles, you probably want useEffect or useLayoutEffect instead.
Parameters
effect
() => (() => void) | void
required
The function with your Effect’s logic. Your effect function can optionally return a cleanup function.
- React will call your effect function before any DOM mutations
- React will call your cleanup function before the component is removed
- If dependencies change, React will run cleanup, then run your effect with new values
deps
Array<mixed> | void | null
The list of all reactive values referenced inside the effect code. Works the same as useEffect dependencies.
- If omitted, the effect runs after every render
- If
[] (empty array), the effect runs only once (on mount)
- If
[dep1, dep2], the effect runs when any dependency changes
Returns
useInsertionEffect returns undefined.
Usage
Injecting dynamic styles from CSS-in-JS libraries
Traditionally, you would style React components using plain CSS:
// In your CSS file
.button {
background: blue;
}
// In your component
function Button() {
return <button className="button">Click me</button>;
}
Some teams prefer to author styles directly in JavaScript code instead of writing CSS files. This usually requires using a CSS-in-JS library or tool. There are three common approaches:
- Static extraction to CSS files with a compiler
- Inline styles:
<div style={{ ... }}>
- Runtime injection of
<style> tags
If you use CSS-in-JS with runtime injection, use useInsertionEffect:
import { useInsertionEffect } from 'react';
// Inside your CSS-in-JS library
let isInserted = new Set();
function useCSS(rule) {
useInsertionEffect(() => {
// As before, prevent duplicate injection
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}
function Button() {
const className = useCSS('button { background: blue; }');
return <button className={className}>Click me</button>;
}
useInsertionEffect is better than injecting styles during render or in useLayoutEffect because it ensures that by the time other Effects run in your components, the <style> tags have already been injected.
How CSS-in-JS libraries use this
Basic style injection
import { useInsertionEffect } from 'react';
const stylesCache = new Map();
function createStyleElement(css) {
const style = document.createElement('style');
style.textContent = css;
return style;
}
function useStyles(css) {
useInsertionEffect(() => {
if (!stylesCache.has(css)) {
const styleElement = createStyleElement(css);
document.head.appendChild(styleElement);
stylesCache.set(css, styleElement);
}
return () => {
const styleElement = stylesCache.get(css);
if (styleElement) {
styleElement.remove();
stylesCache.delete(css);
}
};
}, [css]);
}
// Usage
function Component() {
useStyles(`
.my-component {
background: blue;
color: white;
}
`);
return <div className="my-component">Styled component</div>;
}
CSS-in-JS with dynamic values
import { useInsertionEffect } from 'react';
let styleId = 0;
const injectedStyles = new Map();
function useInlineStyles(styles) {
const classNameRef = useRef(null);
if (!classNameRef.current) {
classNameRef.current = `css-${styleId++}`;
}
const className = classNameRef.current;
useInsertionEffect(() => {
const css = `
.${className} {
${Object.entries(styles)
.map(([key, value]) => `${toKebabCase(key)}: ${value};`)
.join('\n')}
}
`;
if (!injectedStyles.has(className)) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
injectedStyles.set(className, style);
} else {
injectedStyles.get(className).textContent = css;
}
return () => {
const style = injectedStyles.get(className);
if (style) {
style.remove();
injectedStyles.delete(className);
}
};
}, [className, JSON.stringify(styles)]);
return className;
}
function toKebabCase(str) {
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
}
// Usage
function Button({ primary }) {
const className = useInlineStyles({
background: primary ? 'blue' : 'gray',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '4px'
});
return <button className={className}>Click me</button>;
}
Styled components pattern
import { useInsertionEffect, useRef } from 'react';
function styled(Component, stylesFn) {
return function StyledComponent(props) {
const classNameRef = useRef(null);
if (!classNameRef.current) {
classNameRef.current = `styled-${Math.random().toString(36).slice(2)}`;
}
const className = classNameRef.current;
const styles = stylesFn(props);
useInsertionEffect(() => {
const css = `
.${className} {
${styles}
}
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
return () => style.remove();
}, [className, styles]);
return <Component {...props} className={className} />;
};
}
// Usage
const StyledButton = styled(
'button',
(props) => `
background: ${props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`
);
function App() {
return (
<>
<StyledButton>Normal</StyledButton>
<StyledButton primary>Primary</StyledButton>
</>
);
}
Effect Timing Comparison
useInsertionEffect
useLayoutEffect
useEffect
function Component() {
useInsertionEffect(() => {
console.log('1. useInsertionEffect');
// Runs before DOM mutations
// Perfect for injecting styles
});
return <div>Component</div>;
}
Timing: Render → useInsertionEffect → DOM mutations → Layout effects → PaintUse for: Injecting <style> tags for CSS-in-JSfunction Component() {
useLayoutEffect(() => {
console.log('2. useLayoutEffect');
// Runs after DOM mutations, before paint
// Can read layout, cause re-layout
});
return <div>Component</div>;
}
Timing: Render → DOM mutations → useLayoutEffect → PaintUse for: Measuring DOM, synchronous DOM mutationsfunction Component() {
useEffect(() => {
console.log('3. useEffect');
// Runs after paint
// User can see updates before effect runs
});
return <div>Component</div>;
}
Timing: Render → DOM mutations → Paint → useEffectUse for: Data fetching, subscriptions, most side effects
TypeScript
import { useInsertionEffect } from 'react';
function useCSS(css: string): void {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
return () => {
style.remove();
};
}, [css]);
}
// With style object
interface StyleObject {
[key: string]: string | number;
}
function useInlineStyles(styles: StyleObject): string {
const className = `css-${Math.random().toString(36).slice(2)}`;
useInsertionEffect(() => {
const css = Object.entries(styles)
.map(([key, value]) => `${key}: ${value};`)
.join('\n');
const style = document.createElement('style');
style.textContent = `.${className} { ${css} }`;
document.head.appendChild(style);
return () => style.remove();
}, [className, styles]);
return className;
}
Troubleshooting
Why not just use useEffect or useLayoutEffect?
// ❌ Using useLayoutEffect for styles
function Component() {
useLayoutEffect(() => {
// Injects styles AFTER layout has been calculated
// Can cause styles to be applied late, causing flicker
injectStyles();
});
}
// ✅ Using useInsertionEffect for styles
function Component() {
useInsertionEffect(() => {
// Injects styles BEFORE layout is calculated
// Ensures styles are ready when component renders
injectStyles();
});
}
Can I read DOM in useInsertionEffect?
No, you cannot read from the DOM in useInsertionEffect because the DOM hasn’t been updated yet:
// ❌ Don't read DOM in useInsertionEffect
function Component() {
const ref = useRef(null);
useInsertionEffect(() => {
console.log(ref.current); // null! DOM not ready yet
});
return <div ref={ref}>Content</div>;
}
// ✅ Read DOM in useLayoutEffect
function Component() {
const ref = useRef(null);
useLayoutEffect(() => {
console.log(ref.current); // <div>Content</div>
});
return <div ref={ref}>Content</div>;
}
I’m not building a CSS-in-JS library - do I need this?
No! Unless you’re authoring a CSS-in-JS library, you should use:
useEffect for most side effects
useLayoutEffect for measuring DOM or synchronous mutations
- Plain CSS or CSS modules for styling
// ❌ Don't use useInsertionEffect for regular effects
function Component() {
useInsertionEffect(() => {
fetchData(); // Wrong hook!
});
}
// ✅ Use useEffect for regular effects
function Component() {
useEffect(() => {
fetchData();
});
}
Best Practices
Only for CSS-in-JS libraries
useInsertionEffect is a specialized hook for library authors:
// ✅ Good: CSS-in-JS library
function useStyles(css) {
useInsertionEffect(() => {
injectStylesheet(css);
}, [css]);
}
// ❌ Bad: Regular application code
function Component() {
useInsertionEffect(() => {
// You probably want useEffect or useLayoutEffect
doSomething();
});
}
Prevent duplicate injections
const injectedStyles = new Set();
function useCSS(css) {
useInsertionEffect(() => {
if (!injectedStyles.has(css)) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
injectedStyles.add(css);
}
}, [css]);
}
Clean up injected styles
function useCSS(css) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// Clean up when component unmounts
return () => {
style.remove();
};
}, [css]);
}
When to use useInsertionEffect
Use useInsertionEffect when:
- ✅ Building a CSS-in-JS library
- ✅ Injecting
<style> tags dynamically
- ✅ Need to inject styles before layout is calculated
Use useLayoutEffect when:
- ✅ Measuring DOM elements
- ✅ Reading layout (getBoundingClientRect, offsetHeight, etc.)
- ✅ Synchronous DOM mutations before paint
Use useEffect when:
- ✅ Everything else!
- ✅ Data fetching
- ✅ Setting up subscriptions
- ✅ Logging and analytics
- ✅ Most side effects
Real-World Example
Here’s how a CSS-in-JS library might implement a complete solution:
import { useInsertionEffect, useRef } from 'react';
class StyleSheet {
constructor() {
this.styles = new Map();
this.sheet = this.createStyleSheet();
}
createStyleSheet() {
const style = document.createElement('style');
document.head.appendChild(style);
return style.sheet;
}
insert(className, css) {
if (!this.styles.has(className)) {
const index = this.sheet.cssRules.length;
this.sheet.insertRule(`.${className} { ${css} }`, index);
this.styles.set(className, index);
}
return className;
}
delete(className) {
const index = this.styles.get(className);
if (index !== undefined) {
this.sheet.deleteRule(index);
this.styles.delete(className);
}
}
}
const globalStyleSheet = new StyleSheet();
let idCounter = 0;
export function useCSS(css) {
const classNameRef = useRef(null);
if (!classNameRef.current) {
classNameRef.current = `css-${idCounter++}`;
}
const className = classNameRef.current;
useInsertionEffect(() => {
globalStyleSheet.insert(className, css);
return () => {
globalStyleSheet.delete(className);
};
}, [className, css]);
return className;
}
// Usage
function Button({ children, primary }) {
const className = useCSS(`
background: ${primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
`);
return <button className={className}>{children}</button>;
}