State Management in React: Ein praktisches Beispiel
Aktualisiert auf September 24, 2024 19 Minuten gelesen

React ist für viele Entwickler das bevorzugte Framework zum Erstellen dynamischer clientseitiger Anwendungen. Der dynamische Charakter dieser Anwendungen ergibt sich aus der Flexibilität und der erweiterten Liste von Fähigkeiten und Features, die auf der Client-Seite möglich sind und es Entwicklern ermöglichen, in Sekundenschnelle vollwertige Anwendungen zu erstellen, die im Browser geladen werden – eine Leistung, die nicht möglich war Dies war in den Tagen des statischen Webs möglich (oder sehr umständlich).
Mit dieser Erweiterung der Möglichkeiten entstand das Konzept der Zustandsverwaltung. Mit zunehmender Komplexität clientseitiger Anwendungen wächst die Notwendigkeit, den lokalen Zustand beizubehalten, und wird zu einem Flaschenhals an sich, wenn er nicht richtig gehandhabt und unter Berücksichtigung der Skalierbarkeit konzipiert wird.
Dieses Problem wurde von vielen Frameworks angegangen, die unterschiedliche Ansätze verfolgten und sich auf unterschiedliche Teilprobleme konzentrierten. Deshalb ist es wichtig, ein umfassendes Verständnis des Ökosystems des Frameworks der Wahl zu haben, um die Anforderungen jeder Anwendung zu bewerten und entsprechend diesen den richtigen Ansatz zu verwenden Metriken. Dieser Artikel gibt Ihnen einen kurzen Überblick über die häufigsten Probleme bei der Zustandsverwaltung und versucht, verschiedene Ansätze (useState, Context API) als Antwort darauf vorzustellen. Während dieser Artikel mehrere Lösungen vorstellt, konzentriert er sich nur auf Herausforderungen in kleinerem Maßstab. In den kommenden Artikeln werden wir komplexere Themen behandeln.
Authentifizierungs-Workflow
Den im gesamten Artikel vorgestellten Code finden Sie hier.
Auf einen Link zur Live-Vorschau kann hier zugegriffen werden.
Stellen Sie sich den Fall vor, in dem wir den Authentifizierungsprozess für eine React-Anwendung implementieren.
Wie im GIF oben gezeigt, möchten wir Benutzern ermöglichen, sich mit Anmeldeinformationen bei unserer Anwendung anzumelden oder anzumelden. Wenn gültige Anmeldeinformationen angegeben wurden, wird der Benutzer angemeldet, die Anwendung navigiert automatisch zur Startseite und der Benutzer kann mit der Verwendung der Anwendung fortfahren.
Wenn sich der Benutzer abmeldet, werden die Ressourcen der Startseite ebenfalls durch die Anmeldung geschützt und die Anmeldeseite ist die einzige Seite, auf die der Benutzer zugreifen kann.
Wenn wir uns diesen Arbeitsablauf im Hinblick auf die Implementierung vorstellen, hätten wir eine Hauptkomponente mit dem Namen App. Die App-Komponente leitet den Benutzer auf eine von zwei Seiten weiter: Startseite oder Anmelden, wobei der aktuelle Status des Benutzers (angemeldet, abgemeldet) vorgibt, welche Seite Seite, zu der der Benutzer umgeleitet wird, sollte eine Änderung des aktuellen Status des Benutzers (z. B. der Wechsel von angemeldet zu abgemeldet) eine sofortige Weiterleitung zur entsprechenden Seite auslösen.
Wie in der Abbildung oben gezeigt, soll die App-Komponente den aktuellen Status berücksichtigen und nur eine der beiden Seiten – Home oder Login – basierend auf diesem aktuellen Status rendern.
Wenn der Benutzer null ist, bedeutet das, dass wir keinen authentifizierten Benutzer haben. Daher navigieren wir automatisch zur Anmeldeseite und schützen die Startseite durch bedingtes Rendering. Wenn der Benutzer existiert, machen wir das Gegenteil.
Nachdem wir nun ein solides Verständnis davon haben, was implementiert werden sollte, wollen wir einige Optionen erkunden, aber zuerst richten wir unser React-Projekt ein.
Das Projekt-Repo enthält eine Beispiel-Backend-Anwendung, die wir zur Implementierung der Frontend-Seite verwenden werden (wir gehen nicht näher darauf ein, da es hier nicht im Mittelpunkt steht, aber der Code wurde absichtlich einfach gehalten, damit Sie keine Schwierigkeiten damit haben werden). )
Wir beginnen mit der Erstellung der folgenden Seiten und Komponenten:
-
Startseite
-
Anmeldeseite
-
EditUser-Seite
-
Navbar-Komponente
-
UserDropdown-Komponente
Eine React-Anwendung mit mehreren Seiten benötigt eine ordnungsgemäße Navigation. Dafür können wir den React-Router-Dom verwenden, um einen globalen Browser-Router-Kontext zu erstellen und verschiedene React-Routen zu registrieren.
yarn add react-router-dom
Wir wissen, dass viele Leser es vorziehen, den Tutorials zu folgen. Deshalb finden Sie hier die Starter-Vorlage, um Sie auf den neuesten Stand zu bringen. Dieser Starterzweig verwendet DaisyUI für vordefinierte TailwindCSS JSX-Komponenten. Es enthält alle Komponenten, Seiten und den bereits eingerichteten Router. Wenn Sie erwägen, diesem Tutorial zu folgen und den gesamten Authentifizierungsablauf anhand einfacher Schritte selbst zu erstellen, beginnen Sie zunächst mit dem Forken des Repositorys. Nachdem Sie das Repository geforkt haben, klonen Sie es und starten Sie vom start-hereZweig:
git clone git@github.com:<your-username>/fullstack-resourcify.git
Sobald Sie den Start-Hier-Zweig ziehen:
-
Öffnen Sie das Projekt mit Ihrem bevorzugten Code-Editor
-
Verzeichnis in frontend/ ändern
-
Abhängigkeiten installieren: Garn
-
Starten Sie einen Entwicklungsserver: Yarn Dev·
Die Vorschau sollte etwa so aussehen:
Der in der Navigationsleiste (oben rechts) angezeigte Name ist eine Statusvariable, die in der Hauptkomponente der App definiert ist. Dieselbe Variable wird sowohl an die Navigationsleiste als auch an die Startseite übergeben. Das oben verwendete einfache Formular aktualisiert tatsächlich die Statusvariable „name“ aus der EditPage-Komponente.
Auf die Details der Umsetzung gehen die beiden im Folgenden vorgestellten Ansätze ein:
Erster Ansatz: useState
useState()
useState ist einer der am häufigsten verwendeten React-Hooks und ermöglicht es Ihnen, einen Status in einer React-Funktionskomponente zu erstellen und zu ändern. Die useState-Komponente hat eine wirklich einfache Implementierung und ist leicht zu verwenden: Um einen neuen Status zu erstellen, müssen Sie useState mit dem Anfangswert Ihres Status aufrufen und der useState-Hook gibt ein Array zurück, das zwei Variablen enthält: Die erste ist der Status Variable, mit der Sie auf Ihren Status verweisen können, und die zweite eine Funktion, mit der Sie den Wert des Status ändern können: ziemlich einfach.
Wie wäre es, wenn wir das in Aktion sehen würden? Der in der Navigationsleiste (oben rechts) angezeigte Name ist eine Statusvariable, die in der Hauptkomponente der App definiert ist. Dieselbe Variable wird sowohl an die Navigationsleiste als auch an die Startseite übergeben. Das oben verwendete einfache Formular aktualisiert tatsächlich die Statusvariable „name“ aus der EditPage-Komponente. Das Fazit lautet: useState ist ein Kern-Hook, der einen Anfangszustand als Parameter akzeptiert und zwei Variablen mit zwei Werten zurückgibt, die Zustandsvariable, die den Anfangszustand enthält, und eine Setter-Funktion für dieselbe Zustandsvariable.
Lassen Sie uns es aufschlüsseln und sehen, wie das überhaupt umgesetzt wurde.
- Erstellen der Zustandsvariablen „name“:
./src/App.jsx
import { useState } from "react";
function App() {
const testValue = "CLA";
//using the useState hook to create a state variable out of an initial value passed as an argument
const [name, setName] = useState(testValue);
console.log(`Rendering: ${name}`);
return (...)};
Requisiten
Props sind einer der Grundbausteine einer React-Komponente. Wenn Sie sich eine React-Funktionskomponente konzeptionell als Javascript-Funktion vorstellen, dann sind Props nicht mehr als die Funktionsparameter. Die Kombination von Props und dem useState-Hook kann Ihnen ein solides Gerüst bieten zum Verwalten des Status in einer einfachen React-Anwendung.
React-Requisiten werden als Attribute an benutzerdefinierte Komponenten übergeben. Als Requisiten übergebene Attribute können aus dem Requisitenobjekt destrukturiert werden, wenn es als Argument akzeptiert wird, ähnlich wie hier:
Requisiten weitergeben
<Routes>
<Route path="/" element={<Home name={name} />} /> // Passing name as a prop to
Home Component
<Route
path="/user"
element={<EditUser name={name} setName={setName} />} // passing both name and setItem function as props to EditUser component
/>
<Route path="/login" element={<Login />} />
</Routes>
Requisiten können ähnlich wie normale Funktionsargumente innerhalb einer Funktionskomponente akzeptiert und verwendet werden. Da „Name“ als Requisite an die Home-Komponente übergeben wird, können wir ihn in derselben Komponente rendern. Im folgenden Beispiel akzeptieren wir die übergebene Requisite mithilfe der Destrukturierungssyntax, um die Namenseigenschaft aus dem Props-Objekt zu extrahieren.
Requisiten annehmen
./src/pages/Home.jsx
... ...
export default function Home({name}) { //Destructuring the name property from the props object, another approach would be: Home(props.name)
console.log("Rendering: Home");
return (
<div className="flex flex-col bg-white m-auto p-auto">
<h1 className="flex py-5 lg:px-20 md:px-10 mx-5 font-bold text-2xl text-gray-800">
Welcome {name}
</h1>
... ...
Profi-Tipp
Öffnen Sie die Konsole des Browsers und beobachten Sie, wie alle Komponenten, die die Requisite „name“ verwenden, neu gerendert werden, wenn sich der Status ändert. Wenn Sie eine Zustandsvariable manipulieren, speichert React den nächsten Zustand, rendert Ihre Komponente erneut mit den neuen Werten und aktualisiert die Benutzeroberfläche.
useState Nachteile
Requisiten-Bohren
Der Begriff „Props-Drilling“ bezieht sich auf eine Hierarchie von Komponenten, bei der eine Reihe von Komponenten bestimmte Requisiten benötigt, die von einer übergeordneten Komponente bereitgestellt werden. Eine häufige Problemumgehung, die unerfahrene Entwickler normalerweise verwenden, besteht darin, diese Requisiten durch die gesamte Komponentenkette zu leiten, was hier das Problem darstellt Der Ansatz besteht darin, dass eine Änderung an einem dieser Requisiten dazu führt, dass die gesamte Komponentenkette neu gerendert wird, wodurch die gesamte Anwendung aufgrund dieser unnötigen Renderings, also der Komponenten in der Mitte der Kette, die diese Requisiten nicht benötigen, effektiv verlangsamt wird fungieren als Medien zur Übertragung von Requisiten.
Beispiel:
– In der Haupt-App-Komponente wurde mithilfe des useState()-Hooks eine Statusvariable definiert
– Eine Namensstütze wurde an die Navbar-Komponente übergeben
– Dieselbe Requisite wird in der Navbar akzeptiert und noch einmal als Requisite an die UserDropdown-Komponente übergeben
– UserDropdown ist das letzte untergeordnete Element, das die Requisite akzeptiert.
./src/App.jsx
... ...
function App() {
const [name, setName] = useState(test);
console.log("Rendering: App");
return (
<BrowserRouter>
<div className="h-screen">
<Navbar name={name} />
<main className="px-4">
... ...
./src/components/Navbar.jsx
import React from "react";
import Logo from "../assets/cla.svg";
import { BiSearchAlt } from "react-icons/bi";
import { Link } from "react-router-dom";
import UserDropdown from "./UserDropdown";
export default function Navbar({ name }) {
console.log("Rendering: Navbar");
return (
<>
<div className="navbar bg-base-100 drop-shadow-sm">
<div className="flex-1">
<Link
to="/"
className="btn btn-ghost normal-case text-md md:text-xl px-2 gap-1"
>
<img src={Logo} className="h-6" alt="" />
Resources
</Link>
</div>
<UserDropdown name={name} />
</div>
</>
);
}
Stellen Sie sich vor, wie schwierig es ist, eine React-App mit verschiedenen Ebenen von Komponenten zu verwalten, die alle denselben Status übergeben und rendern.
Wachsende Komplexität und Codequalität
Durch die Verwendung von useState und props als einzige Möglichkeit zur Zustandsverwaltung in einer Reaktionsanwendung kann die Codebasis schnell an Komplexität zunehmen und Dutzende oder Hunderte von Zustandsvariablen verwalten müssen, bei denen es sich um Duplikate voneinander handeln kann, die über verschiedene Dateien verstreut sind Komponenten können ziemlich entmutigend sein. Jede Änderung einer bestimmten Zustandsvariablen erfordert eine sorgfältige Berücksichtigung der Abhängigkeiten zwischen Komponenten, um ein mögliches zusätzliches Rendern in einer ohnehin langsamen Anwendung zu vermeiden.
Zweiter Ansatz: Kontext-API
Die Kontext-API ist der Versuch von React, die Nachteile der ausschließlichen Verwendung von Requisiten und useState für die Zustandsverwaltung zu lösen. Insbesondere ist die Kontext-API eine Antwort auf das zuvor erwähnte Problem hinsichtlich der Notwendigkeit, Requisiten über den gesamten Komponentenbaum weiterzugeben. Mithilfe des Kontexts können Sie einen Status für Daten definieren, die Sie als global erachten, und von jedem Punkt im Komponentenbaum aus auf seinen Status zugreifen: kein Bohren mehr mit Props mehr.
Es ist wichtig darauf hinzuweisen, dass die Kontext-API ursprünglich dazu konzipiert wurde, das Problem der globalen Datenfreigabe zu lösen (z. B. UI-Themen, Authentifizierungsinformationen, die unser Anwendungsfall sind, Sprachen usw.) für andere Arten von Daten, die gemeinsam genutzt werden müssen B. zwischen mehr als einer Komponente, aber nicht unbedingt global für die gesamte Anwendung, ist der Kontext möglicherweise nicht die beste Option. Je nach Anwendungsfall können Sie andere Techniken wie Komponentenzusammensetzung in Betracht ziehen /composition-vs-inheritance.html), was den Rahmen dieses Artikels sprengt.
Zum Kontext wechseln
Erstellen des Auth-Kontexts
createContext ist unkompliziert, es erstellt eine neue Kontextvariable und übernimmt einen einzigen optionalen Parameter: den Standardwert der Kontextvariablen.
Sehen wir uns das in Aktion an: Erstellen Sie zunächst einen neuen Ordner „contexts“ und darin eine neue Datei „Auth.jsx“. Um einen neuen Kontext zu erstellen, müssen wir die Funktion createContext() aufrufen und den zurückgegebenen Wert einer neuen Variablen Auth zuweisen, die als Nächstes exportiert wird:
./src/contexts/Auth.jsx
import { createContext } from "react";
export const Auth = createContext();
Geben Sie den Authentifizierungskontext an
Jetzt müssen wir die Kontextvariable „Auth“ offenlegen, die wir zuvor erstellt haben. Um dies zu erreichen, verwenden wir den Kontextanbieter. Der Kontextanbieter ist eine Komponente mit einer „Wert“-Requisite. Wir können diese Requisite verwenden, um einen Wert zu teilen – einen Namen – Im gesamten Komponentenbaum machen wir diesen Wert durch Umschließen des Komponentenbaums mit der Anbieterkomponente von überall im Komponentenbaum aus zugänglich, ohne dass diese Requisite an jede untergeordnete Komponente einzeln weitergegeben werden muss.
In unserem Authentifizierungsbeispiel benötigen wir eine Variable zum Speichern des Benutzerobjekts und eine Art Setter zum Bearbeiten dieser Statusvariablen. Dies ist ein perfekter Anwendungsfall für useState. Wenn Sie Kontext verwenden, müssen Sie sicherstellen, dass Sie die Daten definieren, die Sie bereitstellen möchten, und diese Daten – in unserem Beispiel „Benutzer“ – an den gesamten darin verschachtelten Komponentenbaum übergeben. Definieren Sie daher eine neue Statusvariable „Benutzer“ innerhalb der Anbieterkomponente und Schließlich übergeben wir sowohl user als auch setUser in einem Array als Wert, den der Anbieter dem Komponentenbaum zur Verfügung stellt:
./src/contexts/Auth.jsx
import { createContext, useState } from "react";
export const Auth = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
return <Auth.Provider value={[user, setUser]}>{children}</Auth.Provider>;
};
Wir können auch unsere Statusvariable „name“ von der Haupt-App-Komponente in den Auth-Kontext verschieben und sie verschachtelten Komponenten verfügbar machen:
import { createContext, useState } from "react";
export const Auth = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [name, setName] = useState("CLA");
return <Auth.Provider value={[name, setName]}>{children}</Auth.Provider>;
};
Jetzt müssen wir unsere App nur noch in derselben AuthProvider-Komponente verschachteln, die wir gerade exportiert haben.
./src/main.jsx:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { AuthProvider } from "./contexts/Auth";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<AuthProvider>
<App />
</AuthProvider>
);
Da wir die Kinder-Requisite innerhalb von Auth.Provider rendern, können alle Elemente, die in der AuthProvider-Komponente verschachtelt sind, jetzt die Wert-Requisite nutzen, die wir an Auth.Provider übergeben haben. Es mag verwirrend erscheinen, aber wenn Sie einmal damit experimentiert haben, versuchen Sie, einen globalen Kontextzustand bereitzustellen und zu nutzen. Schließlich machte es für mich erst Sinn, nachdem ich mit der Kontext-API experimentiert hatte.
Den Auth-Kontext nutzen
Der letzte Schritt ist unkompliziert: Wir verwenden den Kontext-Hook „useContext“, um auf den Wert zuzugreifen, den der Kontextanbieter von „Auth“ bereitstellt. In unserem Fall handelt es sich um das Array mit „user“ und „setUser“. Im folgenden Code ist dies möglich Verwenden Sie useContext, um den Auth-Kontext in der Navbar zu nutzen. Dies ist nur möglich, weil Navbar in der App-Komponente verschachtelt ist und da AuthProvider die App-Komponente umschließt, kann die Wertstütze nur mit dem useContext-Hook verwendet werden. Ein weiteres großartiges Tool, das React sofort bereitstellt, um alle Daten zu verwalten, auf die global zugegriffen werden kann und die auch von jeder Verbraucherkomponente manipuliert werden können.
./src/components/Navbar.jsx
export default function Navbar() {
const [name, setName] = useContext(Auth);
console.log(name)
return (...)};
Beachten Sie, dass wir in der Funktionskomponente Navbar() keine Requisiten mehr akzeptieren. Wir verwenden stattdessen useContext(Auth), um den Auth-Kontext zu nutzen und dabei sowohl name als auch setName abzurufen. Das bedeutet auch, dass wir die Requisite nicht mehr an Navbar übergeben müssen:
./src/App.jsx
// ... //
return (
<BrowserRouter>
<div className="h-screen">
<Navbar/> // no need to pass prop anymore
// ... //
Aktualisieren des Auth-Kontexts
Wir können auch die bereitgestellte Funktion setName verwenden, um die Statusvariable „name“ zu manipulieren:
./src/pages/EditUser.jsx
export default function EditUser() { // no need to accept props anymore
const [name, setName] = useContext(Auth); // grabbing the name and setName variables from Auth context
console.log("Rendering: EditUser");
Einführung des useReducer()-Hooks
Im vorherigen Beispiel haben wir die Kontext-API verwendet, um unseren Status im Komponentenbaum zu verwalten und weiterzugeben. Möglicherweise haben Sie bemerkt, dass wir immer noch useState als Basis unserer Logik verwenden. Dies ist größtenteils in Ordnung, da unser Status immer noch ein einfaches Objekt ist Wenn wir jedoch die Möglichkeiten unseres Authentifizierungsablaufs erweitern würden, müssten wir auf jeden Fall mehr als nur die E-Mail-Adresse des aktuell angemeldeten Benutzers speichern, und hier kehren wir zu den Einschränkungen zurück, auf die wir zuvor eingegangen sind Durch die Verwendung von useState mit komplexem Zustand löst React dieses Problem glücklicherweise, indem es eine Alternative zu useState für die Verwaltung komplexer Zustände bereitstellt: Geben Sie useReducer ein.
useReducer kann als verallgemeinerte Version von useState betrachtet werden. Es benötigt zwei Parameter: eine Reduzierfunktion und einen Anfangszustand.
Für den Anfangszustand gibt es nichts Interessantes zu beachten, die Magie passiert innerhalb der Reduzierfunktion: Sie prüft, welche Art von Aktion stattgefunden hat, und abhängig von dieser Aktion bestimmt der Reduzierer, welche Aktualisierungen auf den Zustand angewendet werden sollen, und gibt seinen neuen Wert zurück .
Wenn man sich den folgenden Code ansieht, hat die Reduzierfunktion zwei mögliche Aktionstypen:
-
„LOGIN“: In diesem Fall wird der Benutzerstatus mit den neuen Benutzeranmeldeinformationen aktualisiert, die in der Aktionsnutzlast bereitgestellt werden.
-
„LOGOUT“: In diesem Fall wird der Benutzerstatus aus dem lokalen Speicher entfernt und auf Null zurückgesetzt.
Es ist wichtig zu beachten, dass das Aktionsobjekt sowohl ein Typfeld enthält, das bestimmt, welche Logik angewendet werden soll, als auch ein optionales Nutzlastfeld, um Daten bereitzustellen, die für die Anwendung dieser Logik erforderlich sind.
Schließlich gibt der useReducer-Hook den aktuellen Status und eine Dispatch-Funktion zurück, mit der wir eine Aktion an den Reducer übergeben.
Im Übrigen ist die Logik identisch mit dem vorherigen Beispiel:
./src/contexts/Auth.jsx
import { createContext, useEffect, useReducer, useState } from "react";
export const Auth = createContext();
import { createContext, useReducer } from "react";
export const Auth = createContext();
const reducer = (state, action) => {
switch (action.type) {
case "LOGIN":
return { user: action.payload };
case "LOGOUT":
localStorage.removeItem("user");
return { user: null };
default:
break;
}
};
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, {
user: null,
});
return (
<Auth.Provider value={{ ...state, dispatch }}>{children}</Auth.Provider>
);
};
Aktionen auslösen statt die setState-Setter-Funktion zu verwenden – z. B.: setName –
Wie gerade erwähnt, verwenden wir die Funktion „dispatch“, um eine Aktion an den Reduzierer zu übergeben. Im folgenden Code initiieren wir eine LOGIN-Aktion und stellen die E-Mail-Adresse des Benutzers als Nutzlast bereit. Jetzt wird der Benutzerstatus aktualisiert und diese Änderung wird ausgelöst ein erneutes Rendern aller Komponenten, die den Benutzerstatus abonniert haben. Es ist wichtig darauf hinzuweisen, dass ein erneutes Rendern nur dann ausgelöst wird, wenn eine tatsächliche Änderung des Status auftritt, kein erneutes Rendern, wenn der Reduzierer denselben vorherigen Status zurückgibt.
export default function Login() {
const { dispatch } = useContext(Auth);
const handleLogin = async (e) => {
// Updating the global Auth context
dispatch({ type: "LOGIN", payload: {email: email.current.value} });
};
return (...)};
Benutzeranmeldung
Profi-Tipp
Beachten Sie, dass das Benutzerobjekt, das wir nach einer erfolgreichen Anmeldung erhalten, jetzt im localStorage gespeichert ist.
Benutzerdefinierter Hook für die Anmeldung
Nachdem wir useReducer nun gut im Griff haben, können wir unsere Anmelde- und Abmeldelogik weiter in ihre eigenen separaten benutzerdefinierten Hooks kapseln. Mit einem einzigen Aufruf des Login-Hooks können wir einen API-Aufruf an die Login-Route verarbeiten und den neuen Benutzer abrufen Anmeldeinformationen und speichern Sie sie im lokalen Speicher, senden Sie einen LOGIN-Aufruf, um den Benutzerstatus zu aktualisieren, und kümmern Sie sich gleichzeitig um die Fehlerbehandlung:
./src/hooks/useLogin.jsx
import { useContext, useState } from "react";
import { Auth } from "../contexts/Auth";
export const useLogin = () => {
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const { dispatch } = useContext(Auth);
const login = async (email, password) => {
setIsLoading(true);
setError(null);
try {
const response = await fetch("/api/users/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const json = await response.json();
if (json.name === "Error") {
setError(json.message);
setIsLoading(false);
}
if (!response.ok) {
setIsLoading(false);
setError(json);
}
if (response.ok) {
// Save the user and token in the localstorage
localStorage.setItem("user", JSON.stringify(json));
// Updating the global Auth context
dispatch({ type: "LOGIN", payload: json });
setIsLoading(false);
}
} catch (error) {
console.log(error);
}
};
return { error, isLoading, login };
};
Hinweis: Für fortgeschrittenere React-Benutzer unter den Lesern fragen Sie sich vielleicht, warum wir nicht die Lazy-Initialisierungsfunktion von useReducer verwendet haben, um die Benutzeranmeldeinformationen abzurufen. useReducer akzeptiert einen dritten optionalen Parameter, der als Init-Funktion bezeichnet wird. Diese Funktion wird für den Fall verwendet Wir müssen eine gewisse Logik anwenden, bevor wir den Anfangswert des Zustands erhalten können. Der Grund, warum wir uns nicht dafür entschieden haben, ist eine einfache Frage der Trennung von Belangen. Der Code ist auf diese Weise einfacher zu verstehen und daher einfacher zu warten .
Anmeldeseite
So sieht der obere Teil unserer Anmeldeseite aus, nachdem der useLogin()-Hook zum Extrahieren der Anmeldefunktion verwendet und die Anmeldefunktion mit den von einem Benutzer übermittelten Anmeldeinformationen aufgerufen wurde:
// ... ... //
export default function Login() {
const { login, isLoading, error } = useLogin();
console.log("Rendering: Login");
const email = createRef(null);
const password = createRef(null);
const handleLogin = async (e) => {
await login(email.current.value, password.current.value);
};
return (...)
// ... ... //
Wir deaktivieren auch die Absenden-Funktion, wenn der Benutzer das Formular absendet:
<button
onClick={handleLogin}
disabled={isLoading}
className="btn btn-square w-full bg-gray-100 text-gray-600 hover:bg-gray-300 border-none"
>
{isLoading && "A moment please!"}
{!isLoading && "Login"}
</button>
Und Rendern aller Authentifizierungsfehler, die wir von unserem Backend erhalten:
{
error && <span className="text-red-500 p-2">{error.message}</span>;
}
Beibehalten des Benutzerstatus
Sie fragen sich vielleicht, warum wir das Benutzerobjekt im localStorage speichern müssen. Einfach ausgedrückt: Wir möchten, dass der Benutzer angemeldet bleibt, solange das Token nicht abgelaufen ist. Die Verwendung von localStorage ist eine großartige Möglichkeit, JSON-Teile zu speichern, genau wie in unserem Beispiel. Beachten Sie, dass der Status gelöscht wird, wenn Sie die Seite nach dem Anmelden aktualisieren. Dies kann leicht gelöst werden, indem Sie einen useEffect-Hook verwenden, um zu überprüfen, ob wir ein gespeichertes Benutzerobjekt in localStorage haben. Wenn eines vorhanden ist, melden wir den Benutzer automatisch an:
// ... ... //
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, {
user: null,
});
useEffect(() => {
const user = JSON.parse(localStorage.getItem("user"));
if (user) {
return dispatch({ type: "LOGIN", payload: user });
}
}, []);
return (
<Auth.Provider value={{ ...state, dispatch }}>{children}</Auth.Provider>
);
};
Benutzerdefinierte Hooks für die Abmeldung
Das Gleiche gilt für den Logout-Hook. Hier lösen wir eine LOGOUT-Aktion aus, um die aktuellen Benutzeranmeldeinformationen sowohl aus dem Status als auch aus dem lokalen Speicher zu entfernen:
./src/hooks/useLogout.jsx
import { useContext } from "react";
import { Auth } from "../contexts/Auth";
export const useLogout = () => {
const { dispatch } = useContext(Auth);
const logout = () => {
// delete user from the localstorage
localStorage.removeItem("user");
// Wipe out the Auth context (user:null) / dipatch 'LOGOUT'
dispatch({ type: "LOGOUT" });
};
return { logout };
};
Benutzerabmeldung
Um einen Benutzer abzumelden, fügen wir der Schaltfläche „Abmelden“ in UserDropdown.jsx ein Klickereignis hinzu und behandeln es entsprechend:
./src/components/UserDropdown.jsx
// Extracting the logout function from useLogout() and handling the click event listener //
export default function UserDropdown() {
const { user } = useContext(Auth);
const { logout } = useLogout();
console.log("Rendering: UserDropdown");
const handleLogout = () => {
logout();
};
// ... ... //
// Adding a click event listener to logout button //
<li>
<button onClick={handleLogout}>Logout</button>
</li>
// ... ... //
Reaktionsrouten schützen
Der letzte Schritt bei der Implementierung unserer Anwendung besteht darin, den globalen Benutzerstatus zu nutzen, um die Benutzernavigation zu steuern. Dies ist eine kurze Erinnerung daran, welches Verhalten wir erreichen sollten: Zunächst wird der Benutzer von der Anmeldeseite begrüßt, von da an kann der Benutzer nur noch auf die Startseite zugreifen Nach einer erfolgreichen Anmeldung wird der Benutzer beim Abmelden ebenfalls zur Anmeldeseite weitergeleitet.
Dies erreichen wir mithilfe der React-Router-Dom-Bibliothek, indem wir zwei Routen definieren: „/“ und „/login“. Wir steuern mithilfe des globalen Authentifizierungsstatus, welche Komponente auf jeder Route gerendert werden soll, Authentifizierung, die mit Null ausgewertet wird, stellt einen nicht authentifizierten Benutzer dar und umgekehrt:
./src/App.jsx
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import "./App.css";
import Navbar from "./components/Navbar";
import Home from "./pages/Home";
import Login from "./pages/Login";
import { useContext } from "react";
import { Auth } from "./contexts/Auth";
function App() {
const { user } = useContext(Auth);
return (
<BrowserRouter>
<div className="h-screen">
<Navbar />
<main className="px-4">
<Routes>
<Route
path="/"
element={user ? <Home /> : <Navigate to="/login" />}
/>
<Route
path="/login"
element={!user ? <Login /> : <Navigate to="/" />}
/>
</Routes>
</main>
</div>
</BrowserRouter>
);
}
export default App;
Zusammenfassungsdiagramm
Zusammenfassung
In diesem Artikel haben wir versucht, den einfachen, aber sehr häufigen Anwendungsfall der Implementierung der Zustandsverwaltung für einen Authentifizierungsworkflow anzugehen, indem wir die verschiedenen Ansätze, die Gründe für jeden einzelnen und ihre Kompromisse durchgegangen sind. Die Zustandsverwaltung in clientseitigen Frameworks und insbesondere in React ist eines der am meisten diskutierten Themen in der Frontend-Community, einfach weil sie die Leistung und Skalierbarkeit Ihrer Anwendung beeinträchtigen oder beeinträchtigen kann. Die schiere Menge der verschiedenen Techniken, Muster, Bibliotheken und Tools, die dieses Problem der Zustandsverwaltung zu lösen versuchen, ist überwältigend. Unser Ziel war es, Ihnen ein solides Verständnis der Praktiken zu vermitteln, damit Sie sie in Ihrer eigenen Anwendung implementieren können, für Fortgeschrittene Techniken und Muster der Zustandsverwaltung in komplexeren Anwendungen finden Sie in unserem nächsten Artikel, in dem wir uns mit Redux für skalierbare Zustandsverwaltung in React befassen.
Demnächst erhältlich
ContextAPI vs. Redux Toolkit
Redux wurde bereits viele Jahre vor dem Redux-Toolkit in der Community angenommen. Tatsächlich wurde RTK jedoch als Einstiegsvorlage für das Bootstrap-Redux-Statusmanagement in neuen Anwendungen eingeführt (sein ursprünglicher Name lautete im Oktober 2019 „redux-starter-kit“) Heutzutage besteht zwischen den Redux-Betreuern und der Community ein allgemeiner Konsens darüber, dass das Redux-Toolkit die gültige Möglichkeit ist, mit Redux zu arbeiten. RTX abstrahiert einen Großteil der Redux-Logik, was die Verwendung einfacher und mit weitaus weniger Ausführlichkeit macht und Entwickler dazu ermutigt Befolgen Sie die Best Practices. Ein Hauptunterschied zwischen den beiden besteht darin, dass Redux eigensinnslos entwickelt wurde, eine minimale API bereitstellt und von den Entwicklern erwartet, dass sie den größten Teil der schweren Arbeit übernehmen, indem sie ihre eigenen Bibliotheken für allgemeine Aufgaben schreiben und sich mit der Codestruktur befassen. Dies führte zu einer langsamen Entwicklungszeit und unübersichtlichem Code. Das Redux-Toolkit wurde als zusätzliche Abstraktionsebene hinzugefügt, um zu verhindern, dass Entwickler in die üblichen Fallstricke tappen. Weitere Einblicke und die Argumentation der Betreuer finden Sie in der offiziellen Dokumentation hier.