React hat drei Effect-Hooks: useEffect,
useLayoutEffect und useInsertionEffect. Die
ersten beiden kennen wir. Sie unterscheiden sich im Timing –
useEffect läuft nach dem Paint,
useLayoutEffect davor. useInsertionEffect ist
der dritte, der noch früher läuft. So früh, dass 99% aller
React-Entwickler ihn nie brauchen werden.
Aber die 1%, die ihn brauchen, brauchen ihn wirklich.
CSS-in-JS-Libraries – styled-components, Emotion, Stitches – leben von
diesem Hook. Ohne ihn würden diese Libraries visuell flackern,
Performance-Probleme verursachen oder das Layout mehrfach berechnen
lassen. useInsertionEffect löst ein sehr spezifisches
Problem: Wie füge ich CSS ein, bevor der Browser überhaupt beginnt, das
Layout zu berechnen?
Dieses Kapitel erklärt, warum dieser Hook existiert, wann er gebraucht wird, und warum Sie ihn wahrscheinlich nie direkt verwenden sollten – aber trotzdem indirekt von ihm profitieren, ohne es zu merken.
Um useInsertionEffect zu verstehen, müssen wir das
Rendering-Timing verstehen. React rendert eine Komponente, aktualisiert
das DOM, dann übernimmt der Browser. Aber dieser Prozess ist nicht
monolithisch – er besteht aus klar definierten Phasen.
Die drei Effect-Hooks unterscheiden sich nur im Timing:
useInsertionEffect läuft nach DOM-Mutation, aber vor der Browser-Layout-Berechnung. Das DOM ist aktualisiert, aber der Browser hat noch nicht begonnen, Positionen, Größen oder Stile zu berechnen.
useLayoutEffect läuft nach DOM-Mutation und nach Layout-Berechnung, aber vor dem Paint. Der Browser weiß, wo alles ist und wie groß es ist, hat aber noch nichts auf den Bildschirm gezeichnet.
useEffect läuft nach allem anderen, asynchron und nicht-blockierend. Der Browser hat alles gerendert, der Nutzer sieht die UI.
| Hook | Läuft wann? | DOM-Zugriff | Layout berechnet? | Paint erfolgt? | Blockiert UI? |
|---|---|---|---|---|---|
| useInsertionEffect | Nach DOM-Update | Ja | Nein | Nein | Ja |
| useLayoutEffect | Nach DOM-Update + Layout | Ja | Ja | Nein | Ja |
| useEffect | Nach Paint | Ja | Ja | Ja | Nein |
Das Timing ist präzise und unveränderlich. React garantiert diese Reihenfolge.
Stellen wir uns vor, wir verwenden styled-components:
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
padding: 10px 20px;
border-radius: 4px;
`;
function App() {
return <Button primary>Click me</Button>;
}
Was passiert hier? styled.button ist keine echte
DOM-Komponente, sondern eine Wrapper-Funktion. Wenn Button
rendert, generiert styled-components zur Laufzeit CSS-Regeln basierend
auf den Props. Diese CSS-Regeln müssen dem DOM hinzugefügt werden –
typischerweise als <style>-Tag im
<head>.
Aber wann sollen sie hinzugefügt werden?
Option 1: useEffect
useEffect(() => {
const style = document.createElement('style');
style.textContent = `.button-abc { background: blue; }`;
document.head.appendChild(style);
}, []);
Problem: useEffect läuft nach dem Paint. Der Browser
zeichnet den Button zuerst ohne Styles, dann mit Styles. Der Nutzer
sieht einen Flash of Unstyled Content (FOUC) – der Button erscheint kurz
ungestylt, dann springt er zur richtigen Darstellung.
Option 2: useLayoutEffect
useLayoutEffect(() => {
const style = document.createElement('style');
style.textContent = `.button-abc { background: blue; }`;
document.head.appendChild(style);
}, []);
Besser, aber nicht perfekt. useLayoutEffect läuft vor
dem Paint, aber nach der Layout-Berechnung. Der Browser
berechnet das Layout ohne die Styles, fügt sie dann ein, und muss das
Layout erneut berechnen. Doppelte Arbeit.
Bei einem einzelnen Button ist das vernachlässigbar. Bei einer komplexen App mit Hunderten von styled-components, die bei jedem Rendering neue Styles generieren, wird es zum Performance-Problem. Jedes Re-Rendering könnte doppelte Layout-Berechnungen auslösen – Layout Thrashing.
Option 3: useInsertionEffect
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = `.button-abc { background: blue; }`;
document.head.appendChild(style);
}, []);
Perfekt. useInsertionEffect läuft nach der DOM-Mutation,
aber vor der ersten Layout-Berechnung. Die Styles sind im DOM,
bevor der Browser überhaupt beginnt zu rechnen. Eine Layout-Berechnung,
kein Thrashing, kein FOUC.
Die API ist vertraut:
useInsertionEffect(() => {
// Setup-Code
return () => {
// Cleanup-Code
};
}, [dependencies]);
Exakt wie useEffect und useLayoutEffect.
Der Unterschied liegt nicht in der Syntax, sondern im Timing.
Ein minimales Beispiel einer CSS-Injection:
function StyledComponent({ className, children }: Props) {
const styleId = useId();
useInsertionEffect(() => {
const style = document.createElement('style');
style.setAttribute('data-styled', styleId);
style.textContent = `.${className} { color: red; font-weight: bold; }`;
document.head.appendChild(style);
return () => {
style.remove();
};
}, [styleId, className]);
return <div className={className}>{children}</div>;
}
Beim Mount fügt useInsertionEffect ein
<style>-Tag ein. Beim Unmount entfernt die
Cleanup-Funktion es. Das alles passiert vor der ersten
Layout-Berechnung.
styled-components, Emotion und ähnliche Libraries verwenden
useInsertionEffect intern. Als Nutzer dieser Libraries
sehen Sie den Hook nie direkt. Aber er läuft bei jedem Rendering Ihrer
gestylten Komponenten.
Ein vereinfachtes Modell, wie styled-components arbeiten könnte:
function createStyledComponent(tag: string, styles: string) {
return function StyledComponent(props: any) {
const className = useMemo(() => generateClassName(styles, props), [props]);
const css = useMemo(() => generateCSS(className, styles, props), [className, props]);
useInsertionEffect(() => {
injectCSS(css);
return () => {
removeCSS(css);
};
}, [css]);
return React.createElement(tag, { ...props, className });
};
}
// Verwendung
const Button = createStyledComponent('button', `
background: blue;
padding: 10px;
`);
Bei jedem Rendering: 1. Props ändern sich 2. Neue CSS-Regeln werden
generiert (useMemo) 3. useInsertionEffect
injiziert die neuen Regeln 4. React rendert das
<button> mit der korrekten className 5.
Browser berechnet Layout mit den Styles
Ohne useInsertionEffect (vor React 18) mussten Libraries
useLayoutEffect verwenden, mit den beschriebenen
Performance-Implikationen. Oder sie mussten komplexe Workarounds
implementieren, um Styles serverseitig zu extrahieren und clientseitig
zu hydratisieren.
useInsertionEffect ist ausschließlich für
CSS-Injection gedacht. React’s Dokumentation warnt explizit:
This is intended for CSS-in-JS libraries. Unless you’re building a CSS-in-JS library and need a place to inject styles, you probably want useEffect or useLayoutEffect instead.
Verwenden Sie useInsertionEffect NICHT für:
Alle diese Szenarien gehören in useEffect oder
useLayoutEffect.
// ❌ FALSCH: DOM-Messung in useInsertionEffect
useInsertionEffect(() => {
const height = ref.current?.getBoundingClientRect().height;
setHeight(height); // Layout noch nicht berechnet!
}, []);
// ✓ RICHTIG: useLayoutEffect für DOM-Messungen
useLayoutEffect(() => {
const height = ref.current?.getBoundingClientRect().height;
setHeight(height);
}, []);
// ❌ FALSCH: API-Call in useInsertionEffect
useInsertionEffect(() => {
fetch('/api/data').then(setData);
}, []);
// ✓ RICHTIG: useEffect für Side Effects
useEffect(() => {
fetch('/api/data').then(setData);
}, []);
Die Versuchung mag groß sein, useInsertionEffect als
“super-frühen Effect” zu verwenden, aber das führt zu Bugs. Das DOM ist
möglicherweise noch nicht vollständig konsistent, Layout-Informationen
sind nicht verfügbar, und State-Updates können unvorhersehbar sein.
Wie alle Effect-Hooks läuft useInsertionEffect nur im
Browser, nicht auf dem Server.
function StyledComponent() {
useInsertionEffect(() => {
console.log('Läuft nur im Browser');
}, []);
return <div>Content</div>;
}
Beim SSR wird der Effect-Code komplett ignoriert. Das ist korrekt – CSS-Injection ist eine Browser-Operation. Für SSR haben CSS-in-JS-Libraries separate Mechanismen:
// Server-seitiger Code (vereinfacht)
function renderToString(Component: ReactNode) {
const { html, css } = extractCriticalCSS(Component);
return `
<style>${css}</style>
<div id="root">${html}</div>
`;
}
// Client-seitiger Code
function hydrate() {
// Styles sind bereits im <head>, keine Injection nötig
ReactDOM.hydrate(<App />, document.getElementById('root'));
}
CSS-in-JS-Libraries extrahieren die kritischen Styles serverseitig,
fügen sie dem HTML hinzu, und hydratisieren clientseitig ohne erneute
Injection. useInsertionEffect kommt nur bei clientseitigen
Updates ins Spiel.
useInsertionEffect ist synchron und blockierend. Er
läuft, bevor der Browser das Layout berechnet, aber das heißt auch: Er
blockiert die Layout-Berechnung, bis er abgeschlossen ist.
Deshalb muss der Code in useInsertionEffect
extrem schnell sein. Idealerweise nur DOM-Operationen –
createElement, appendChild,
setAttribute. Keine Loops, keine komplexen Berechnungen,
keine async Operations.
// ✓ Gut: Schnelle DOM-Operation
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}, [css]);
// ❌ Schlecht: Langsame Berechnung
useInsertionEffect(() => {
const css = items.map(item =>
generateComplexCSS(item) // Teure Operation!
).join('\n');
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}, [items]);
// ✓ Besser: Berechnung in useMemo, Injection in useInsertionEffect
const css = useMemo(() =>
items.map(generateComplexCSS).join('\n'),
[items]
);
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}, [css]);
Die Berechnung der Styles sollte in useMemo erfolgen
(oder sogar außerhalb der Komponente, wenn möglich).
useInsertionEffect sollte nur die Injection
durchführen.
// Einfachste CSS-in-JS-Implementierung
const styleCache = new Map<string, HTMLStyleElement>();
function css(template: TemplateStringsArray, ...values: any[]): string {
// Generiere CSS-String
const cssString = template.reduce((acc, str, i) => {
return acc + str + (values[i] ?? '');
}, '');
// Generiere eindeutige Klasse
const hash = hashString(cssString);
const className = `css-${hash}`;
return JSON.stringify({ className, cssString });
}
function useStyles(styleData: string) {
const { className, cssString } = JSON.parse(styleData);
useInsertionEffect(() => {
// Prüfe, ob Style bereits injiziert
if (styleCache.has(className)) {
return;
}
// Erstelle und injiziere Style
const style = document.createElement('style');
style.setAttribute('data-css', className);
style.textContent = `.${className} { ${cssString} }`;
document.head.appendChild(style);
styleCache.set(className, style);
return () => {
// Cleanup bei Unmount
style.remove();
styleCache.delete(className);
};
}, [className, cssString]);
return className;
}
// Verwendung
function Button({ primary }: { primary?: boolean }) {
const className = useStyles(css`
background: ${primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`);
return <button className={className}>Click me</button>;
}
Diese minimale Implementierung zeigt das Kernprinzip: 1. CSS wird als
String generiert 2. Ein eindeutiger Klassenname wird erstellt 3.
useInsertionEffect injiziert den Style 4. Die Komponente
verwendet den Klassennamen
Produktions-Libraries wie styled-components sind natürlich deutlich komplexer – sie handhaben Theming, SSR, Vendor-Prefixing, Critical CSS Extraction, und mehr. Aber das Grundprinzip bleibt gleich.
React DevTools zeigen useInsertionEffect wie andere
Hooks an. Bei Komponenten mit vielen gestylten Children kann die
Hook-Liste lang werden:
Button
└─ useId
└─ useInsertionEffect
└─ useMemo
└─ ...
Wenn Sie Performance-Probleme vermuten, prüfen Sie: 1. Wird
useInsertionEffect zu oft aufgerufen? 2. Sind die
Style-Strings stabil (via useMemo)? 3. Werden alte Styles
korrekt entfernt (Cleanup-Funktion)?
Browser DevTools zeigen die injizierten
<style>-Tags:
<head>
<style data-styled="active">
.css-abc123 { background: blue; }
</style>
<style data-styled="active">
.css-def456 { color: red; }
</style>
<!-- Hunderte mehr bei großen Apps -->
</head>Zu viele <style>-Tags können die Performance
beeinträchtigen. Moderne CSS-in-JS-Libraries aggregieren Styles oder
verwenden CSS Variables, um die Tag-Anzahl zu reduzieren.
useInsertionEffect ist eine Lösung für ein spezifisches
Problem: Runtime CSS-Generation. Aber es gibt Alternativen, die ohne
Runtime-Overhead auskommen:
CSS Modules – Styles zur Build-Zeit generieren, keine Runtime-Injection.
Static CSS-in-JS (Linaria, vanilla-extract) – Schreibe CSS-in-JS-Syntax, extrahiere zur Build-Zeit zu statischem CSS.
Tailwind CSS – Utility-Classes, kein Runtime-Overhead.
CSS-in-TS (Panda CSS, StyleX) – Typsichere Styles, zur Build-Zeit extrahiert.
Diese Ansätze eliminieren die Notwendigkeit für
useInsertionEffect komplett, indem sie die Style-Generation
aus der Runtime in die Build-Zeit verschieben. Das ist oft schneller und
einfacher.
Aber für Libraries, die echte Runtime-CSS-Generation benötigen – weil
sie hochdynamische Theming unterstützen oder vollständig zur Laufzeit
komponierbare Styles bieten – bleibt useInsertionEffect
unverzichtbar.
useInsertionEffect ist ein Hook, den Sie wahrscheinlich
nie direkt verwenden werden. Aber Sie profitieren von ihm jedes Mal,
wenn Sie styled-components, Emotion oder eine ähnliche Library
nutzen.
Das ist ein Pattern, das sich durch React’s Design zieht: Hooks für
Endnutzer (useState, useEffect) und Hooks für
Library-Autoren (useSyncExternalStore,
useInsertionEffect). Diese Trennung ermöglicht es, komplexe
Probleme auf der richtigen Abstraktionsebene zu lösen.
Als Anwendungsentwickler ist Ihr wichtigstes Wissen über
useInsertionEffect: 1. Er existiert für CSS-in-JS 2. Er
läuft vor der Layout-Berechnung 3. Sie sollten ihn nicht direkt
verwenden
Wenn Sie eine CSS-in-JS-Library bauen, ist
useInsertionEffect Ihr bester Freund. Für alle anderen:
Wissen Sie, dass er da ist, verstehen Sie, warum er existiert, und
überlassen Sie die Nutzung den Experten, die ihn wirklich brauchen.