navigation Navigation


useLayoutEffect()


Der useLayoutEffect Hook ist eine spezielle Variante von useEffect in React, die synchron direkt nach allen DOM-Mutationen ausgeführt wird - noch bevor der Browser die Änderungen rendert. Dieser Hook ist essentiell für Operationen, die exakte DOM-Messungen erfordern oder visuelle Updates durchführen müssen, bevor der Benutzer Änderungen sehen kann. Im Gegensatz zum standardmäßigen useEffect, der asynchron nach dem Rendering ausgeführt wird, blockiert useLayoutEffect den Browser vom Aktualisieren des Bildschirms, bis die Callback-Funktion abgeschlossen ist. Diese Funktionalität macht ihn unverzichtbar für präzise DOM-Manipulationen, kann jedoch bei komplexen Berechnungen zu Leistungseinbußen führen.

Inhaltsverzeichnis

    Einführung

    Aufbau

    Stellen wir uns vor, React baut eine Webseite wie ein Bühnenbild auf.

    1. Erste Phase: Der Render-Pass (render)
      • React nimmt die Components und deren aktuellen Zustand (Props und State).
      • Es erstellt eine virtuelle Repräsentation davon, wie die Webseite aussehen soll (das Virtual DOM).
      • Dann vergleicht es diese neue virtuelle Repräsentation mit der vorherigen und berechnet die minimal notwendigen Änderungen am echten DOM (also dem, was der Browser tatsächlich anzeigt).
    2. Zweite Phase: DOM-Mutationen
      • React wendet diese berechneten Änderungen auf das echte DOM an. Elemente werden hinzugefügt, entfernt oder aktualisiert.
      • Wichtig: Zu diesem Zeitpunkt hat der Browser diese Änderungen noch nicht auf dem Bildschirm dargestellt (gezeichnet). Die Bühne ist umgebaut, aber der Vorhang ist noch zu.
    3. Hier kommt der Unterschied: useEffect vs. useLayoutEffect
      • useEffect (Standardfall)
        • Wann wird es ausgeführt? Nachdem React die Änderungen ins DOM geschrieben hat und nachdem Browser diese Änderungen auf dem Bildschirm gezeichnet hat.
        • Wie wird es ausgeführt? Asynchron. Das bedeutet, es blockiert nicht den Browser. Der Nutzer sieht die gerenderte Seite und dann, kurz danach, läuft der Code in useEffect.
        • Vorteil: Die Seite bleibt responsive, da der Browser nicht auf das Ausführen des Effekts warten muss. Ideal für die meisten Seiteneffekte wie Daten laden, Event-Listener setzen etc.
      • useLayoutEffect (Spezialfall)
        • Wann wird es ausgeführt? Nachdem React die Änderungen ins DOM geschrieben hat, aber bevor der Browser diese Änderungen auf dem Bildschirm gezeichnet hat.
        • Wie wird es ausgeführt? Synchron. Das bedeutet, es blockiert den Browser. React wartet, bis der Code in useLayoutEffect komplett ausgeführt ist, bevor es dem Browser erlaubt, die Seite zu zeichnen.
        • Nachteil (und Grund zur Vorsicht): Wenn der useLayoutEffect Code lange braucht, verzögert es das erste Malen der Seite und kann zu einer spürbar langsameren Benutzererfahrung führen.

    Warum braucht man useLayoutEffect

    Die Haupt-Verwendung für useLayoutEffect ist, wenn man das DOM liest (z.B. Dimensionen oder Positionen von Elementen messen) und dann synchron das DOM ändern möchte, bevor der Benutzer eine Zwischenstufe sieht, die vielleicht komisch aussieht (ein “Flickern”).

    Als Beispiel kann man sich vorstellen, man möchte die Höhe eines Elements messen und basierend darauf die Position eines anderen Elements anpassen.

    Beispiel 1 - Mit useEffect

    In diesem Beispiel werden die Höhe eines Elements und die Position eines Tooltips verändern.

    Mit useEffect
    import { useState, useEffect, useRef } from 'react';
    
    function ExampleWithUseEffect() {
        const [contentHeight, setContentHeight] = useState(0);
        const refContent = useRef(null);
    
        // Simuliert dynamischen Inhalt
        const [text, setText] = useState('Kurzer Text');
        useEffect(() => {
            setTimeout(() => {
                setText('Ein viel längerer Text, der die Höhe verändert.')
            }, 100);
        }, []);
    
        useEffect(() => {
            // Läuft nachdem der Browser gezeichnet hat.
            if (refContent.current) {
                const newHeight = refContent.current.offsetHeight;
                console.log('useEffect - Gemessene Höhe:', newHeight);
                setContentHeight(newHeight);
            }
        }, [text]);
    
        // Tooltip-Stil wird basierend auf contentHeight berechnet
        const tooltipStyle = {
            position: 'absolute',
            bottom: `${contentHeight + 5}px`,
            left: 20,
            backgroundColor: 'lightyellow',
            border: '1px solid orange',
            padding: 5,
            transition: 'bottom 0.1s ease-out'
        };
    
        return (
            <div style={{
                position: 'relative',
                marginTop: 60,
                padding: 20,
                border: '1px solid lightblue'
            }}>
                <h3 style={{marginBottom: 40}}>Mit useEffect</h3>
                <div
                    ref={refContent}
                    style={{
                        border: '1px solid green',
                        width: 200
                    }}
                >
                    {text}
                </div>
                {contentHeight > 0 && (
                    <div style={tooltipStyle}>
                        Tooltip: (Höhe: {contentHeight})
                    </div>
                )}
            </div>
        );
    }

    Als Ergebnis haben wir ein leicht springendes, flickerndes Verhalten, wenn wir die Seite mehrmals neualden.

    React Hook - useLayoutEffect - Beispiel 1 mit useEffect

    Beispiel 1 - Mit useLayoutEffect

    Im zweiten Teil schreiben wir unser Beispiel um, sodass wir diesen flickernden Effekt vermeiden.

    Mit useLayoutEffect
    import { useState, useEffect, useRef, useLayoutEffect } from 'react';
    
    function ExampleWithUseLayoutEffect() {
    
        const [contentHeight, setContentHeight] = useState(0);
        const refContent = useRef(null);
    
        // Simuliert dynamischen Inhalt
        const [text, setText] = useState('Kurzer Text');
        useEffect(() => {
            setTimeout(() => {
                setText('Ein viel längerer Text, der die Höhe verändert.');
            }, 100);
        }, []);
    
        useLayoutEffect(() => {
            // Läuft NACH den DOM-Manipulationen,
            // ABER BEVOR der Browser zeichnet.
            if (refContent.current) {
                const newHeight = refContent.current.offsetHeight;
                console.log('useLayoutEffect - Gemessene Höhe:', newHeight);
                setContentHeight(newHeight);
            }
        }, [text]);
    
        const tooltipStyle = {
            position: 'absolute',
            bottom: `${contentHeight + 5}px`,
            left: 20,
            backgroundColor: 'lightgoldenrodyellow',
            border: '1px solid gold',
            padding: 5
        };
    
        return (
            <div style={{
                position: 'relative',
                marginTop: 60,
                padding: 20,
                border: '1px solid lightblue'
            }}>
                <h3 style={{marginBottom: 40}}>Mit useLayoutEffect</h3>
                <div
                    ref={refContent}
                    style={{
                        width: 200,
                        border: '1px solid darkseagreen'
                    }}
                >
                    {text}
                </div>
                {contentHeight > 0 && (
                    <div style={tooltipStyle}>
                        Tooltip (Höhe: {contentHeight})
                    </div>
                )}
            </div>
        );
    }
    
    export default ExampleWithUseLayoutEffect;

    Damit ist das Verhalten etwas anders. Hier gibt es kein Flickern beim Laden der Seite. Um es visuell deutlicher zu machen, habe ich beide Elemente nebeneinander gesetzt.

    React Hook - useLayoutEffect - Beispiel 1 mit useLayoutEffect