navigation Navigation


Mehrere Zustandswerte


In React-Komponenten kann die Verwaltung mehrerer Zustandswerte notwendig sein, um verschiedene Aspekte der Benutzeroberfläche unabhängig voneinander zu steuern. Mit dem useState()-Hook lassen sich beliebig viele separate Zustände definieren, was eine flexible und feingranulare Steuerung ermöglicht. Je nach Komplexität des Zustands bietet sich auch eine strukturierte Bündelung oder der Einsatz alternativer Hooks an.

Inhaltsverzeichnis

    Einleitung

    Wenn man etwas komplexere Anwendungen baut, wird man nahezu immer mehrere Zustandswerte benötigen. Sehr oft ist es nicht nur ein Feld oder ein Element, das einen Wert in einem Component setzen und überwachen soll. Manchmal braucht man eine Überwachung von ganzem Warenkorb, mit Produkten, Preisen und Sonstigem. Manchmal sind es Post-Beiträge, auf die Benutzer in bestimmter Art und Weise reagieren können. Es gibt viele Anwendungsbeispiele, in denen das Benutzer-Interface dynamisch aktualisiert werden soll.

    Um dieses Thema etwas anschaulicher zu machen, denken wir uns ein Beispiel-Szenario aus. Stellen wir uns vor, wir hätten zwei Eingabefelder. Ein Feld für den Vornamen und ein Feld für den Nachnamen. Unser Component soll die eingegebene Werte in diese beiden Felder überwachen.

    Da wir mit zwei Eingabe-Elementen arbeiten, brauchen wir auch zwei Zustandswerte: Den eingegebenen Vornamen und den eingegebenen Nachnamen.

    Verwendung von mehreren State Slices

    Man kann in React mit mehreren Zustandswerten, oder auch State Slices genannt, einfach arbeiten, indem man useState() mehrere Male im Component aufruft.

    Hier das Code-Beispiel, das unser Beispiel-Szenario aufzeigen soll.

    StateExample.jsx
    import { useState } from 'react';
    
    const SimpleRegisterForm = () => {
        const [enteredFirstname, setEnteredFirstname] = useState('');
        const [enteredLastname, setEnteredLastname] = useState('');
    
        const handleUpdateFirstname = (event) => {
            setEnteredFirstname(event.target.value);
        };
    
        const handleUpdateLastname = (event) => {
            setEnteredLastname(event.target.value);
        };
    
        return (
            <form>
                <input
                    type="text"
                    placeholder="Dein Vorname"
                    onBlur={handleUpdateFirstname}
                />
                <input
                    type="text"
                    placeholder="Dein Nachname"
                    onBlur={handleUpdateLastname}
                />
            </form>
        );
    };
    
    export default SimpleRegisterForm;

    In diesem Beispiel werden zwei Zustandswerte (State Slices) verwaltet und entsprechend wir Funktion useState() zwei Mal aufgerufen. Diese zwei Werte können unabhängig voneinander gelesen und aktualisiert werden.

    Namenskonvention für Handler

    In diesem Component wurden zwei Handler-Funktionen verwendet, die als Zwischen-Funktionen eingesetzt werden, um die Update-Funktion der Zustandsverwaltung aufzurufen. Die Namen beider Funktion beginnen mit handle.... Das ist keine harte Verpflichtung, allerdings eine Namenskonvention unter React-Entwicklern.

    Man muss dieser Konvention nicht zwangsläufig folgen. Solange die Funktion unmissverständliche und beschreibende Namen haben, ist alles in Ordnung.

    Diese Funktion können man auch firstnameUpdateHandler und lastnameUpdateHandler nennen.

    Man kann so viele State Slices registrieren, wie viele man benötigt. In der Regel wird man ein paar Zustandswerte haben, da man hoffentlich das Ziel verfolgt, komplexere Components in kleinere Components aufzusplitten. Entsprechenderweise würden sich auch die Zustandswerte (State Slices) von der Anzahl her auf die Components verteilen.

    Der Vorteil von mehreren State Slices ist die Möglichkeit die Zustandswerte einzeln zu aktualisieren.

    Ein Nachteil bei Verwendung von vielen Zustandswerten könnte der größere Code sein.

    Man kann allerdings, als Alternative zu mehreren Zustandswerten, auch einen kombinierten (zusammengeführten) Zustand-Objekt verwenden.

    Verwendung von Merged State Object

    Statt jedes Mal useState() Funktion für jeden Zustandswert aufzurufen, kann man ein größeres Daten- bzw. Zustands-Objekt definieren, welches alle notwendigen Werte kombiniert/einschließt.

    Wir schreiben nun das Beispiel von oben so um, dass wir ein Daten-Objekt verwenden und nur einmal useState() aufrufen.

    StateExample.jsx
    import { useState } from 'react';
    
    const CombinedRegisterForm = () => {
        const [userData, setUserData] = useState({
            firstname: '',
            lastname: ''
        });
    
        const handleUpdateFirstname = event => {
            setUserData({
                firstname: event.target.value,
                lastname: userData.lastname
            });
        };
    
        const handleUpdateLastname = event => {
            setUserData({
                firstname: userData.firstname,
                lastname: event.target.value
            });
        };
    
        return (
            <div>
                <p>Firstname: {userData.firstname ? userData.firstname : '---'}</p>
                <p>Lastname: {userData.lastname ? userData.lastname : '---'}</p>
                <hr/>
                <form>
                    <input
                        type="text"
                        onInput={handleUpdateFirstname}
                    />
                    <input
                        type="text"
                        onInput={handleUpdateLastname}
                    />
                </form>
            </div>
        );
    
    };
    
    export default CombinedRegisterForm;

    React State - useState - Merged object

    In diesem Beispiel wurde useState() nur einmal aufgerufen und dabei wurde das initiale Objekt, mit initialen Startwerten übergeben. Das Objekt beinhaltete zwei Eigenschaften firstname und lastname. Die Eigenschaften können natürlich frei gewählt werden.

    Die Funktion useState() gibt, wie bereits an anderen Stellen erwähnt, ein Array mit zwei Elementen zurück.

    Wichtiger Hinweis

    Die Art und Weise, wie die Funktion zum Aktualisieren des Zustandes (in diesem Fall setUserData) verwendet wurde, ist nicht ganz korrekt. Diese Verwendungsart funktioniert, verstößt jedoch gegen ein paar Best Practices in React. Weiter im Verlauf wird die korrekte Verwendung gezeigt.

    Wenn man Zustandsobjekte so aktualisiert, wie eben im letzten Beispiel gezeigt, gibt es eine Sache, die man im Hinterkopf behalten sollte: Man muss immer alle Eigenschaften des Objektes angeben. Auch die, die sich nicht ändern. Alle Eigenschaften müssen deswegen angegeben werden, da man sonst inkonsistenten Datenstand hätte.

    Wenn man also nur die Eigenschaften des Zustandsobjekts angeben würde, die man ändern möchte, würden all die nicht angegeben Eigenschaften verloren gehen. Die Funktion setzt einen neuen Wert, also ein neues Objekt und dieses beinhaltet eben nur das, was man explizit angibt.

    Das ist der Grund, warum wir im vorherigen Beispiel die Eigenschaften, die sich nicht ändern sollten, mit den bereits im Zustandsobjekt vorhanden Werten wieder beschrieben haben.

    setUserData({
        firstname: event.target.value,
        lastname: userData.lastname
    });
    setUserData({
        firstname: userData.firstname,
        lastname: event.target.value
    });

    Korrekte Verwendung von Updates

    Wie wir bereits verstehen, ist es bei Einsatz von Objekten als Zustandshalter unter Umständen möglich, das gesamte Zustands-Objekt zu überschreiben und Daten zu verlieren.

    Deswegen ist es wichtig bei Arrays und Objekten als Zustands-Datentypen immer auch alle bestehende Eigenschaften/Elemente zum neuen Array/Objekt hinzuzufügen. Nicht nur die geänderten.

    Im Allgemeinen gilt die Regel (und nicht nur für Arrays oder Objekte), dass Status-Updates, die vom vorherigen Status (Zustandswerten) abhängen, sollten mithilfe einer Funktion durchgeführt werden, die an die Status-Aktualisierungsfunktion übergeben wird.

    Weniger korrekte Verwendung

    Zuerst schauen wir uns ein Beispiel an, welches technisch Funktioniert, allerdings gegen Best Practices von React verstößt.

    UpdateExampleIncorrect.jsx
    import { useState } from 'react';
    
    const IncorrectUpdate = () => {
        const [counter, setCounter] = useState(0);
    
        const handleCounterUpdate = () => {
            setCounter(counter + 1);
        };
    
        return (
            <>
                <p>Counter: {counter}</p>
                <button onClick={handleCounterUpdate}>Increment</button>
            </>
        );
    
    };
    
    export default IncorrectUpdate;

    Wie schon gesagt, funktioniert dieser Code problemlos.

    React State - Beispiel mit einem Counter Component


    Korrekte Verwendung

    Nun schreiben wir dieses Beispiel so um, dass die Verwendung der Update-Funktion korrekt ist.

    Wir übergeben eine anonyme Funktion als Parameter an die setCounter Funktion in dem Fall, die als Parameter den aktuellen Wert des Zustandwertes annimmt und einen modifizierten Zustandswert zurückgibt.

    UpdateExampleIncorrect.jsx
    import { useState } from 'react';
    
    const CorrectUdpate = () => {
        const [counter, setCounter] = useState(0);
    
        const handleCounterUpdate = () => {
            setCounter((prevCounter) => prevCounter + 1);
        };
    
        return (
            <>
                <p>Counter: {counter}</p>
                <button onClick={handleCounterUpdate}>Increment</button>
            </>
        );
    };
    
    export default CorrectUdpate;

    Die Funktion an sich kann man auch traditioneller schreiben, falls es für jemanden einfacher ist.

    const handleCounterUpdate = () => {
        setCounter((prevCounter) => {
            return prevCounter + 1;
        });
    };

    Oder noch traditioneller.

    const handleCounterUpdate = () => {
        setCounter(function(prevCounter) {
            return prevCounter + 1;
        });
    };

    Oder man lagert die Funktion komplett aus, wenn es dafür wirklich gute Gründe gibt.

    const valueUpdater = currentValue => {
        return currentValue + 1;
    };
    
    const handleCounterUpdate = () => {
        setCounter(valueUpdater);
    };

    Hier, für die eher Anfänger unter den Lesern, die beiden Funktionen in klassischer Fassung.

    function valueUpdater(currentValue) {
        return currentValue + 1;
    }
    
    function handleCounterUpdate() {
        setCounter(valueUpdater);
    }

    React State - Beispiel mit einem Counter Component


    Technische Details

    Auf den ersten Blick könnte diese Herangehensweise etwas seltsam aussehen. Einerseits übergeben wir direkt einen bestimmten neuen Wert an die Updater-Funktion und sie aktualisiert den entsprechenden Zustand, andererseits übergeben wir eine Funktion.

    Technisch gesehen, eine Funktion wird übergeben als Parameter an die Updater-Funktion. React wird jedoch diese Funktion nicht als den neuen Zustandswert verwenden. Stattdessen, wenn eine Funktion übergeben wird, ruft React diese Funktion auf und übergibt automatisch als Parameter den aktuellen Zustandswert an diese (übergebene) Funktion.

    Diese Funktion sollte unbedingt einen Wert zurückgeben. Dieser wird dann von React als der neue Zustandswert übernommen.

    Und, weil React automatisch den aktuellen Zustandswert an die Funktion übergibt, können wir also diesen Wert für die Errechnung des neuen Wertes verwenden.

    Objekte und Arrays als Zustandswerte - korrekte Aktualisierung

    An dieser Stellen greifen das Beispiel von oben (mit zwei Eingabefeldern) erneut auf und werden auf die korrekte Art und Weise in Bezug auf die Aktualisierung umschreiben.

    StateExample.jsx
    import { useState } from 'react';
    
    const CombinedRegisterForm = () => {
        const [userData, setUserData] = useState({
            firstname: '',
            lastname: ''
        });
    
        const handleUpdateFirstname = event => {
            setUserData(prevData => ({
                firstname: event.target.value,
                lastname: prevData.lastname
            }));
        };
    
        const handleUpdateLastname = event => {
            setUserData(prevData => ({
                firstname: prevData.firstname,
                lastname: event.target.value
            }));
        };
    
        return (
            <div>
                <p>Firstname: {userData.firstname ? userData.firstname : '---'}</p>
                <p>Lastname: {userData.lastname ? userData.lastname : '---'}</p>
                <hr/>
                <form>
                    <input
                        type="text"
                        onInput={handleUpdateFirstname}
                    />
                    <input
                        type="text"
                        onInput={handleUpdateLastname}
                    />
                </form>
            </div>
        );
    
    };
    
    export default CombinedRegisterForm;