navigation Navigation


Einführung


React Context ist ein mächtiges Werkzeug zur Verwaltung von Zuständen und Daten, die über die Komponentenhierarchie hinweg geteilt werden sollen, ohne sie explizit über Props weiterzureichen. Es ermöglicht eine zentrale Datenquelle, die von beliebigen Komponenten im Baum direkt abgerufen und aktualisiert werden kann. Dadurch vereinfacht Context vor allem die Handhabung globaler Zustände wie Benutzerinformationen, Theme-Einstellungen oder Spracheinstellungen, ohne auf komplexe State-Management-Bibliotheken zurückgreifen zu müssen.

Inhaltsverzeichnis

    Problem - Prop-Weitergabe

    In React fließen die Daten standardmäßig von oben nach unten (Eltern zu Kindern) durch Props. Diese unidirektionale Datenübertragung ist normalerweise gut verständlich und nachvollziehbar. Aber was passiert, wenn wir Daten an tief verschachtelte Komponenten weitergeben müssen?

    Wir werden zuerst ein Beispiel erstellen, in dem das sogenannte Prop-Drilling eindeutig zu sehen ist. Sprich, in diesem Beispiel reichen wir von einem Component zum nächsten eine bestimmte Zustands-Eigenschaft durch. Dabei benötigen einige Components diese Eigenschaft bzw. diese Information gar nicht und reichen sie nur weiter.

    In folgenden Beispiel wird gezeigt, wie man bestimmte Daten durch mehrere Components an das Ziel-Component übergeben kann.

    UserComponent.jsx
    const UserProfile = ({ username }) => {
        return <span>Hallo, {username}</span>
    };
    
    const Navigation = ({ username }) => {
        return (
            <nav>
                <UserProfile username={username} />
            </nav>
        );
    };
    
    const Header = ({ username }) => {
        return (
            <div>
                <Navigation username={username} />
            </div>
        );
    };
    
    const UserComponent = () => {
        const username = 'John';
        return <Header username={username} />
    };
    
    export default UserComponent;

    Klar, technisch funktioniert es und wir erhalten die Daten korrekt an allen Stellen, an die wir diese Daten übergeben. Allerdings ist es ziemlich umständlich. Die Annahme von Props durch bestimmte Components, die die Informationen gar nicht benötigen ist auch nicht sinnvoll.

    Lösung - Context API

    Mit der Context API kann man einen “Container” für die Daten erstellen, auf den jede Komponente im Baum zugreifen kann, ohne dass Zwischen-Komponenten involviert sein müssen.

    Dieser Container wird mithilfe von Provider aufgebaut. Dieser muss an der obersten Ebene positioniert werden. Gemeint ist die Stelle, ab welcher die innliegenden Components auf die Daten zugreifen müssen.

    UserComponent.jsx
    import { createContext, useContext } from 'react';
    
    // Container für Daten erzeugen
    const UserContext = createContext();
    
    const UserProfile = () => {
        const username = useContext(UserContext);
        return <span>Hallo, {username}</span>
    };
    
    const Navigation = () => {
        return (
            <nav>
                <UserProfile />
            </nav>
        );
    };
    
    const Header = () => {
        return (
            <div>
                <Navigation />
            </div>
        );
    };
    
    const UserComponent = () => {
        const username = 'John';
    
        return (
            <UserContext.Provider value={username}>
                <Header />
            </UserContext.Provider>
        );
    };
    
    export default UserComponent;

    In diesem Beispiel ist Header unser Top-Component. Das heißt, dass alle anderen Components Kind-Components sind. In diesem Fall müssen wir um dieses Component unseren Provider als eine Art Wrapper platzieren (sodass die anderen Components Kinder von diesem Provider bzw. Wrapper werden).

    Irgendwo in unserer Datei erzeugen wir diesen Container für Daten.

    const UserContext = createContext();

    Eigentlich könnte man dies auch in eine andere Datei auslagern, per export bereitstellen und an benötigten Stellen wieder importieren. Das ist eine reine Organisationsangelegenheit.

    Im UserProfile Component verwenden wir useContext(UserContext), um diesen “Container” mit Daten zu verwenden. Wir lesen den aktuellen Stand aus und speichern diesen in der Variable username;

    const UserProfile = () => {
        const username = useContext(UserContext);
        return <span>Hallo, {username}</span>
    };

    Das UserComponent ist unsere oberste Ebene für alle anderen Components aus diesem Beispiel. Hier definieren wir unseren Datenbestand. In diesem Beispiel ist sehr überschaubar.

    const username = 'John';

    Bevor wir unser Entry-Component (Header) verwenden, müssen wir diese mit dem UserContext Component umgeben. Wir stellen sogenannten Provider bereit. Als Wert value übergeben wir unseren Variable username mit dem Wert John.

    return (
        <UserContext.Provider value={username}>
            <Header />
        </UserContext.Provider>
    );

    Die Formel für die Anwendung von Context über einen Provider lautet wie folgt.

    <MyContext.Provider value={/* irgendein Wert */}>
        {/* Kind-Komponenten, die auf den Context zugreifen können */}
    </MyContext.Provider>

    Beispiel - Objekt als Wert

    Die Funkton createContext() wird verwendet, um das Context-Objekt zu erstellen. Dabei kann die Funktion einen Parameter, den Startwert annehmen.

    In diesem Beispiel werden wir einige Dinge anders machen.

    • Wir lagern die Erstellung eines Contextes in einen eigenen Ordner und eine eigene Datei aus.
    • Wir werden nicht mit einem primitiven Wert, sondern mit einem Objekt als Wert des Context-Containers arbeiten.
    • Wir verwenden State Management, um bestimmte Werte im Context-Container zu aktualisieren.

    1 - Context-Datei

    Wir starten mit der Erstellung der Context-Datei. Dort erzeugen wir das Context-Objekt und setzen die initiale Daten. Wir arbeiten in diesem Beispiel mit einem Benutzer-Objekt.

    src/state/UserContext.jsx
    import { createContext } from 'react';
    
    const initUserData = () => {
        return {
            name: 'John Doe',
            email: 'john@outlook.com',
            age: 20,
            job: 'Developer'
        };
    };
    
    const UserContext = createContext(initUserData());
    
    export default UserContext;

    Damit haben wir unsere Context-Datei definiert.

    Wenn wir einen Log unmittelbar nach dem Aufruf der createContext Funktion platzieren, werden wir unsere initiale Daten in der Konsolenausgabe sehen.

    src/state/UserContext.jsx
    // ...
    
    const UserContext = createContext(initUserData());
    console.log(UserContext);
    
    export default UserContext;

    React Context - Objekt als initialer Wert - Ausgabe in der Konsole

    Ab hier werden wir schrittweise das Beispiel ausbauen, um möglichst stufenweise das Verständnis zu steigern.


    2 - Grundstruktur definieren

    Zunächst definieren wir die Grundstruktur. Unser Ziel ist es aufzuzeigen, dass Prop Drilling bei größerem Component-Baum mit Context API kein Problem mehr ist. Hierfür definieren wir ein paar Components. Die Components werden ineinander verschachtelt sein.

    src/components/UseContextObject.jsx
    const ComponentTop = () => {
        return (
            <>
                <ComponentMiddle />
            </>
        );
    };
    
    const ComponentMiddle = () => {
        return (
            <>
                <ComponentBottom />
            </>
        );
    };
    
    const ComponentBottom = () => {
        return (
            <button>Update email</button>
        );
    };
    
    const UseContextObject = () => {
        return (
            <ComponentTop />
        )
    };
    
    export default UseContextObject;

    Das Prinzip ist hier vermutlich gut erkennbar. Wir bauen mit diesen Components eine kleine Hierarchie auf. Unser zentrales Component UseContextObject ist unser Einstiegspunkt, das Root-Component in diesem Context, sozusagen.


    3 - State definieren

    Da wir vorhaben in diesem Beispiel bestimmte Daten zu aktualisieren und zwangsläufig irgendwo diese zwischenzuspeichern, verwenden wir den useState Hook. Der Zustand wird mit den initialen Daten gesetzt, die wir über UserContext erhalten, welche wir in der extra Datei definiert haben.

    Außerdem definieren wir eine Funktion updateUserData zum Aktualisieren der Daten.

    src/components/UseContextObject.jsx
    import { useState } from 'react';
    import UserContext from '../../state/UserContext';
    
    const ComponentTop = () => {
        return (
            <>
                <ComponentMiddle />
            </>
        );
    };
    
    const ComponentMiddle = () => {
        return (
            <>
                <ComponentBottom />
            </>
        );
    };
    
    const ComponentBottom = () => {
        return (
            <button>Update email</button>
        );
    };
    
    const UseContextObject = () => {
    
        const [userData, setUserData] = useState(UserContext._currentValue);
    
        const updateUserData = (newData) => {
            setUserData(current => newData);
        };
    
        return (
            <ComponentTop />
        )
    };
    
    export default UseContextObject;

    Somit haben wir unser State definiert. Als initialen Wert haben wir den aktuellen Datenbestand unseres Context-Containers übergeben UserContext._currentValue.

    Zusätzlich haben wir eine Funktion definiert, welche wir aufrufen werden, wenn Daten am Objekt userData aktualisiert werden sollen.


    4 - Context Provider

    Nun möchten wir im ComponentTop Component unsere Benutzerdaten ausgeben. Aktuell weiß dieses Component nichts über Benutzer und hat keinen Zugriff auf irgendwelche Daten. Ohne Weiteres wäre es also nicht möglich.

    Damit alle Kind-Komponenten Zugriff auf den Datenbestand im Context-Container erhalten, müssen sie mit dem Context-Provider umgeben werden.

    Man kann diesen an beliebiger Stelle einsetzen. Wichtig zu beachten, dass nur Components innerhalb von diesem Provider Zugriff auf die Daten haben.

    Also werden wir nun unser Entry-Component ComponentTop in der Funktion des UseContextObject Components in diesem Context-Provider setzen.

    Außerdem müssen wir einen Wert übergeben. An dieser Stelle übergeben wir ein zusammengesetztes Objekt, bestehend aus Benutzerdaten und der Funktion zum Aktualisieren der Benutzerdaten.

    { userData, updateUserData }

    Wir ergänzen nun unseren Haupt-Component und setzen unseren Einstiegs-Component in den Context-Provider.

    src/components/UseContextObject.jsx
    import { useState } from 'react';
    import UserContext from '../../state/UserContext';
    
    const ComponentTop = () => {
        return (
            <>
                <ComponentMiddle />
            </>
        );
    };
    
    const ComponentMiddle = () => {
        return (
            <>
                <ComponentBottom />
            </>
        );
    };
    
    const ComponentBottom = () => {
        return (
            <button>Update email</button>
        );
    };
    
    const UseContextObject = () => {
        const [userData, setUserData] = useState(UserContext._currentValue);
    
        const updateUserData = (newData) => {
            setUserData(current => newData);
        };
    
        return (
            <UserContext.Provider value={{ userData, updateUserData }}>
                <ComponentTop />
            </UserContext>
        )
    };
    
    export default UseContextObject;

    5 - Zugriff auf den Context

    Da wir jetzt unsere Components (weil sie alle Kind-Components von ComponentTop sind) mit dem Zugriff auf den Context ausgestattet haben, können wir wie geplant im ComponentTop die Benutzerdaten anzapfen und ausgeben.

    Dafür verwenden wir den useContext Hook, welcher uns eine bequeme Möglichkeit bereitstellt, um auf die Daten im Context-Container zuzugreifen.

    src/components/UseContextObject.jsx
    import { useState, useContext } from 'react';
    import UserContext from '../../state/UserContext';
    
    const ComponentTop = () => {
        const { userData } = useContext(UserContext);
    
        return (
            <>
                <div className="user_info">
                    <h3>User Info</h3>
                    <p>Name: {userData.name}</p>
                    <p>E-Mail: {userData.email}</p>
                    <p>Age: {userData.age}</p>
                    <p>Job: {userData.job}</p>
                </div>
                <ComponentMiddle />
            </>
        );
    };
    
    const ComponentMiddle = () => {
        return (
            <>
                <ComponentBottom />
            </>
        );
    };
    
    const ComponentBottom = () => {
        return (
            <button>Update email</button>
        );
    };
    
    const UseContextObject = () => {
        const [userData, setUserData] = useState(UserContext._currentValue);
    
        const updateUserData = (newData) => {
            setUserData(current => newData);
        };
    
        return (
            <UserContext.Provider value={{ userData, updateUserData }}>
                <ComponentTop />
            </UserContext>
        )
    };
    
    export default UseContextObject;

    Damit haben wir bereits unsere Ausgabe ermöglicht, ohne die Props zu verwenden.

    React Context - Ausgabe von Benutzerdaten aus dem Context ohne Props


    6 - Aktualisierung der Daten

    Im abschließenden Schritt müssen wir dafür sorgen, dass wir die Daten aktualisieren können. Wir möchten es im untersten Component im Komponenten-Baum tun, nämlich in ComponentBottom.

    Was benötigen wir in diesem Component? Wir haben dort bereits unseren Button. Für diesen müssen wir einen Handler definieren, der unseren Klick-Event verarbeitet. Diese Handler-Funktion verknüpfen wir mit dem onClick Event.

    Zusätzlich müssen wir die, im Context-Objekt, vorhandenen Daten auslesen. Die Daten auslesen können wir aus unserem UserContext Context-Objekt mithilfe der Funktion useContext, wie wir es bereits an der anderen Stelle getan haben.

    src/components/UseContextObject.jsx
    import { useState, useContext } from 'react';
    import UserContext from '../../state/UserContext';
    
    const ComponentTop = () => {
        const { userData } = useContext(UserContext);
    
        return (
            <>
                <div className="user_info">
                    <h3>User Info</h3>
                    <p>Name: {userData.name}</p>
                    <p>E-Mail: {userData.email}</p>
                    <p>Age: {userData.age}</p>
                    <p>Job: {userData.job}</p>
                </div>
                <ComponentMiddle />
            </>
        );
    };
    
    const ComponentMiddle = () => {
        return (
            <>
                <ComponentBottom />
            </>
        );
    };
    
    const ComponentBottom = () => {
        const { userData, updateUserData } = useContext(UserContext);
    
        const updateEmail = () => {
            updateUserData({ ...userData, email: 'john@gmail.com' });
        };
    
        return (
            <button onClick={updateEmail}>Update email</button>
        );
    };
    
    const UseContextObject = () => {
        const [userData, setUserData] = useState(UserContext._currentValue);
    
        const updateUserData = (newData) => {
            setUserData(current => newData);
        };
    
        return (
            <UserContext.Provider value={{ userData, updateUserData }}>
                <ComponentTop />
            </UserContext>
        )
    };
    
    export default UseContextObject;

    Was passiert im ComponentBottom genau? Wir lesen unser UserContext Objekt aus. Wir wissen, dass dort zwei Elemente gehalten werden. userData, ein Objekt mit den Benutzerdaten und updateUserData, eine Referenz auf die Funktion, welche die Benutzerdaten aktualisiert.

    Wir haben einen Klick-Handler updateEmail registriert. Diese Funktion wird bei jedem Klick auf den Button aufgerufen. Diese Funktion ruft wiederrum die Funktion zum Aktualisieren der Benutzerdaten aus updateUserData auf. Nachdem diese Funktion aufgerufen wurde, ruft sie die setUserData Update-Funktion für unseren State auf.

    React Context Objekt - Beispiel mit initialen Daten
    Initiale Daten
    React Context Objekt - Beispiel mit aktualisierten Daten
    Aktualisierte Daten