Zarządzanie stanem w React: praktyczny przykład

reagowanie
zarządzanie stanem
kontekstowe API
Zarządzanie stanem w React: praktyczny przykład cover image

React to dla wielu programistów podstawowy framework do tworzenia dynamicznych aplikacji po stronie klienta. Dynamiczny charakter tych aplikacji wynika z elastyczności oraz rozszerzonej listy możliwości i funkcji dostępnych po stronie klienta, co umożliwiło programistom tworzenie pełnoprawnych aplikacji ładowanych w przeglądarce w ciągu kilku sekund, co było wyczynem, którego nie było możliwe (lub bardzo kłopotliwe) w czasach statycznej sieci.

Wraz z tym rozszerzeniem możliwości pojawiła się koncepcja zarządzania stanem, ponieważ w miarę wzrostu złożoności aplikacji po stronie klienta, potrzeba utrzymywania stanu lokalnego rośnie, stając się wąskim gardłem samym w sobie, jeśli nie zostanie poprawnie obsługiwane i pomyślane z myślą o skalowalności.

Kwestię tę rozwiązano w wielu frameworkach, stosując różne podejścia i koncentrując się na różnych zestawach podproblemów, dlatego ważne jest, aby mieć wysoki poziom zrozumienia ekosystemu wybranego frameworka, aby ocenić potrzeby każdej aplikacji i zastosować właściwe podejście zgodnie z tymi metryka. W tym artykule znajdziesz krótki przegląd typowych problemów związanych z zarządzaniem stanem i spróbujesz wprowadzić różne podejścia (useState, Context API) jako odpowiedź na nie. Chociaż w tym artykule zostanie przedstawionych wiele rozwiązań, skupimy się jedynie na wyzwaniach na mniejszą skalę, bardziej zaawansowane tematy omówimy w nadchodzących artykułach.

Przebieg uwierzytelniania

Kod przedstawiony w całym artykule można znaleźć tutaj.

Link do podglądu na żywo można uzyskać tutaj.

Rozważmy przypadek, w którym wdrażamy proces uwierzytelniania dla aplikacji React.

User Login

Jak pokazano na powyższym GIF-ie, chcemy umożliwić użytkownikom logowanie się lub rejestrację w naszej aplikacji przy użyciu poświadczeń. Jeżeli podano prawidłowe dane uwierzytelniające, użytkownik zostanie zalogowany, aplikacja automatycznie przejdzie do strony głównej i użytkownik będzie mógł kontynuować korzystanie z aplikacji.

Podobnie, jeśli użytkownik się wyloguje, zasoby strony głównej będą chronione po zalogowaniu, strona logowania będzie jedyną stroną dostępną dla użytkownika.

Myśląc o tym przepływie pracy pod kątem implementacji, mielibyśmy główny komponent o nazwie App, komponent App przekieruje użytkownika na jedną z dwóch stron: Strona główna lub Logowanie, bieżący stan użytkownika (zalogowany, wylogowany) będzie dyktował, która strony, na którą następuje przekierowanie, zmiana aktualnego stanu użytkownika (np. zmiana z zalogowanego na wylogowanego) powinna spowodować natychmiastowe przekierowanie na odpowiednią stronę.

State

Jak pokazano na powyższej ilustracji, chcemy, aby komponent aplikacji uwzględniał bieżący stan i renderował tylko jedną z dwóch stron – stronę główną lub logowanie – w oparciu o ten bieżący stan.

Jeśli użytkownik ma wartość null, oznacza to, że nie mamy żadnego uwierzytelnionego użytkownika, dlatego automatycznie przechodzimy do strony logowania i zabezpieczamy stronę główną za pomocą renderowania warunkowego. Jeśli użytkownik istnieje, postępujemy odwrotnie.

Teraz, gdy mamy już solidne pojęcie o tym, co należy wdrożyć, przeanalizujmy kilka opcji, ale najpierw skonfigurujmy nasz projekt React,

Repozytorium projektu zawiera przykładową aplikację backendową, której użyjemy do implementacji strony frontendowej (nie będziemy się w to zagłębiać, ponieważ nie na tym się skupiamy, ale kod został celowo utrzymany w prostocie, więc nie będziesz miał z nim trudności )

Zaczynamy od utworzenia następujących stron i komponentów:

  • Strona główna

  • Strona logowania

  • Edytuj stronę użytkownika

  • Komponent paska nawigacyjnego

  • Komponent UserDropdown

Aplikacja React z wieloma stronami wymaga odpowiedniej nawigacji, w tym celu możemy użyć React-router-dom do stworzenia globalnego kontekstu routera przeglądarki i zarejestrowania różnych tras React.


yarn add react-router-dom

Wiemy, że wielu czytelników woli śledzić samouczki, więc oto szablon startowy, który pomoże Ci przyspieszyć. Ta gałąź startowa używa DaisyUI dla predefiniowanych komponentów TailwindCSS JSX. Zawiera wszystkie komponenty, strony i router już skonfigurowany. Jeśli rozważasz skorzystanie z tego samouczka i samodzielne zbudowanie całego procesu uwierzytelniania, wykonując proste kroki, zacznij od rozwidlenia repozytorium. Po rozwidleniu repozytorium sklonuj je i zacznij od start-heregałęzi:


git clone git@github.com:<your-username>/fullstack-resourcify.git

Po pociągnięciu gałęzi start-tutaj:

  • Otwórz projekt za pomocą preferowanego edytora kodu

  • Zmień katalog na frontend/

  • Zainstaluj zależności: przędza

  • Uruchom serwer deweloperski: dev przędzy ·

Podgląd powinien wyglądać mniej więcej tak:

Changing user name

Nazwa wyświetlana na pasku nawigacyjnym – w prawym górnym rogu – jest zmienną stanu zdefiniowaną w głównym komponencie aplikacji. Ta sama zmienna jest przekazywana zarówno do paska nawigacyjnego, jak i do strony głównej. Prosty formularz użyty powyżej faktycznie aktualizuje zmienną stanu „nazwa” ze składnika EditPage.

Dwa podejścia przedstawione poniżej zostaną omówione w szczegółach implementacji:

Pierwsze podejście: useState

stan użycia()

useState to jeden z najczęściej używanych haków React, pozwala tworzyć i mutować stan w komponencie funkcjonalnym React. Komponent useState ma naprawdę prostą implementację i jest łatwy w użyciu: aby utworzyć nowy stan, musisz wywołać useState z początkową wartością twojego stanu, a hak useState zwróci tablicę zawierającą dwie zmienne: pierwsza to stan zmienna, której możesz użyć do odniesienia się do swojego stanu, a druga to funkcja, której używasz do zmiany wartości stanu: całkiem proste.

A może zobaczymy to w akcji? Nazwa wyświetlana na pasku nawigacyjnym – w prawym górnym rogu – jest zmienną stanu zdefiniowaną w głównym komponencie aplikacji. Ta sama zmienna jest przekazywana zarówno do paska nawigacyjnego, jak i do strony głównej. Prosty formularz użyty powyżej faktycznie aktualizuje zmienną stanu „nazwa” ze składnika EditPage. Konkluzja jest następująca: useState to rdzeń, który akceptuje stan początkowy jako parametr i zwraca dwie zmienne zawierające dwie wartości, zmienną stanu zawierającą stan początkowy oraz funkcję ustawiającą dla tej samej zmiennej stanu.

Rozłóżmy to na czynniki pierwsze i zobaczmy, jak zostało to wdrożone.

  1. Utworzenie zmiennej stanu „nazwa”:

./src/Aplikacja.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 (...)};

Rekwizyty

Pod względem koncepcyjnym rekwizyty są jednym z podstawowych elementów składowych komponentu reagującego. Jeśli pomyślisz o komponencie funkcjonalnym React jako o funkcji JavaScript, to rekwizyty są niczym więcej niż parametrami funkcji, połączenie rekwizytów i haka useState może zapewnić solidną strukturę do zarządzania stanem w prostej aplikacji React.

Rekwizyty React są przekazywane jako atrybuty do niestandardowych komponentów. Atrybuty przekazane jako props mogą zostać zniszczone z obiektu props po przyjęciu go jako argumentu, podobnie jak poniżej:

Przekazywanie rekwizytów

<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>

Rekwizyty mogą być akceptowane i używane wewnątrz komponentu funkcjonalnego, podobnie jak zwykłe argumenty funkcji. To dlatego, że „nazwa” jest przekazywana jako rekwizyt do komponentu Home, możemy ją wyrenderować w tym samym komponencie. W poniższym przykładzie akceptujemy przekazaną właściwość, używając składni destrukturyzującej w celu wyodrębnienia właściwości name z obiektu props.

Akceptowanie rekwizytów

./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>
... ...

Profesjonalna wskazówka

Otwórz konsolę przeglądarki i zwróć uwagę, jak wszystkie komponenty korzystające z właściwości „nazwa” są ponownie renderowane, gdy zmienia się stan. Podczas manipulacji zmienną stanu React zapisze następny stan, ponownie wyrenderuje komponent z nowymi wartościami i zaktualizuje interfejs użytkownika.

Components Re-rendering

Wady stanu użycia

Podpory – wiercenie

Wiercenie rekwizytów to termin odnoszący się do hierarchii komponentów, w której zestaw komponentów wymaga pewnych rekwizytów dostarczonych przez komponent nadrzędny. Typowym obejściem, które zwykle stosują niedoświadczeni programiści, jest przekazywanie tych rekwizytów przez cały łańcuch komponentów. Problem z tym podejście jest takie, że zmiana któregokolwiek z tych rekwizytów spowoduje ponowne renderowanie całego łańcucha komponentów, skutecznie spowalniając całą aplikację w wyniku niepotrzebnych renderowań, czyli komponentów w środku łańcucha, które nie wymagają tych rekwizytów pełnić rolę nośnika do przenoszenia rekwizytów.

Przykład:

  • Zdefiniowano zmienną stanu w głównym komponencie aplikacji przy użyciu haka useState().

  • Do komponentu Navbar przekazano właściwość nazwy

  • Ten sam element zaakceptowany w pasku nawigacyjnym i ponownie przekazany jako element do komponentu UserDropdown

  • UserDropdown jest ostatnim elementem podrzędnym, który akceptuje właściwość.

Prop-drilling

./src/Aplikacja.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>
    </>
  );
}

Wyobraź sobie, jak trudno jest utrzymać aplikację React z różnymi warstwami komponentów, z których wszystkie przekazują i renderują ten sam stan.

Rosnąca złożoność i jakość kodu

Używając useState i props jako jedynego sposobu zarządzania stanem w aplikacji reagującej, baza kodu może szybko stać się złożona i wymagać zarządzania dziesiątkami lub setkami zmiennych stanu, które mogą być swoimi duplikatami, rozproszonymi w różnych plikach i komponentów może być dość zniechęcające, każda zmiana danej zmiennej stanu będzie wymagała dokładnego rozważenia zależności pomiędzy komponentami, aby uniknąć potencjalnego dodatkowego renderowania w i tak już powolnej aplikacji.

Drugie podejście: kontekstowe API

Context API to próba rozwiązania przez React niedogodności związanych z używaniem samych właściwości i useState do zarządzania stanem. W szczególności kontekstowe API stanowi odpowiedź na wspomniany wcześniej problem dotyczący konieczności przekazywania rekwizytów w dół całego drzewa komponentów. Za pomocą kontekstu możesz zdefiniować stan danych, który uważasz za globalny i uzyskać dostęp do jego stanu z dowolnego punktu drzewa komponentów: koniec z wierceniem podporowym.

Należy podkreślić, że kontekstowy interfejs API został pierwotnie zaprojektowany w celu rozwiązania problemu globalnego udostępniania danych, danych takich jak motywy interfejsu użytkownika, informacji uwierzytelniających, które są naszym przypadkiem użycia, języków itp.) w przypadku innych typów danych, które należy udostępnić pomiędzy więcej niż jednym komponentem, ale niekoniecznie globalnym dla całej aplikacji, kontekst może nie być najlepszą opcją, w zależności od przypadku użycia, możesz rozważyć inne techniki, takie jak skład komponentów, co wykracza poza zakres tego artykułu.

Przełączanie na kontekst

Tworzenie kontekstu uwierzytelniania

createContext jest proste, tworzy nową zmienną kontekstową, przyjmuje jeden opcjonalny parametr: domyślną wartość zmiennej kontekstowej.

Zobaczmy to w akcji, najpierw utwórz nowy folder „contexts” i wewnątrz niego nowy plik „Auth.jsx”. Aby utworzyć nowy kontekst należy wywołać funkcję createContext(), przypisać zwróconą wartość do nowej zmiennej Auth, która następnie zostanie wyeksportowana:

./src/contexts/Auth.jsx

import { createContext } from "react";

export const Auth = createContext();

Podaj kontekst uwierzytelniania

Teraz musimy udostępnić zmienną kontekstową „Auth”, którą utworzyliśmy wcześniej, aby to osiągnąć, używamy dostawcy kontekstu, dostawca kontekstu to komponent, który ma właściwość „wartość”, możemy użyć tej właściwości, aby udostępnić wartość – nazwę – w całym drzewie komponentów, owijając drzewo komponentów komponentem dostawcy, udostępniamy tę wartość z dowolnego miejsca w drzewie komponentów, bez konieczności przekazywania tej właściwości każdemu komponentowi potomnemu z osobna.

W naszym przykładzie uwierzytelniania potrzebujemy zmiennej do przechowywania obiektu użytkownika i pewnego rodzaju modułu ustawiającego do manipulowania tą zmienną stanu. Jest to idealny przypadek użycia useState. Korzystając z Kontekstu, musisz upewnić się, że definiujesz dane, które chcesz dostarczyć, i przekazujesz te dane – w naszym przykładzie użytkownika – do wszystkich zagnieżdżonych w nim drzew komponentów, więc zdefiniuj nową zmienną stanu user wewnątrz komponentu dostawcy i na koniec przekazujemy zarówno user, jak i setUser wewnątrz tablicy jako wartość, którą dostawca udostępni drzewu komponentów:

./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>;
};

Kolejną rzeczą, którą możemy zrobić, to przenieść naszą zmienną stanu „nazwa” z głównego komponentu aplikacji do kontekstu Auth i udostępnić ją zagnieżdżonym komponentom:

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>;
};

Teraz jedyne, co pozostało, to zagnieżdżenie naszej aplikacji w tym samym komponencie AuthProvider, który właśnie wyeksportowaliśmy.

./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>
);

Ponieważ renderujemy właściwość podrzędną w Auth.Provider, wszystkie elementy zagnieżdżone w komponencie AuthProvider mogą teraz korzystać z właściwości wartości, którą przekazaliśmy do Auth.Provider. Może się to wydawać zagmatwane, ale gdy już z tym poeksperymentujesz, spróbuj zapewnić i wykorzystać stan globalny – Kontekst. W końcu nabrało to dla mnie sensu dopiero po eksperymentowaniu z Context API.

Korzystanie z kontekstu uwierzytelniania

Ostatni krok jest prosty. Używamy haka kontekstowego „useContext”, aby uzyskać dostęp do wartości dostarczanej przez dostawcę kontekstu „Auth”, czyli w naszym przypadku tablicy zawierającej użytkownika i setUser. W poniższym kodzie możemy użyj useContext, aby skorzystać z kontekstu uwierzytelniania w pasku nawigacyjnym. Jest to możliwe tylko dlatego, że pasek nawigacyjny jest zagnieżdżony w komponencie aplikacji, a ponieważ AuthProvider otacza komponent aplikacji, właściwość wartości można wykorzystać wyłącznie za pomocą haka useContext. Kolejne niesamowite narzędzie React zapewnia gotowe do użycia zarządzanie dowolnymi danymi, do których można uzyskać dostęp globalnie, a także którymi może manipulować dowolny komponent konsumencki.

./src/components/Navbar.jsx

export default function Navbar() {
 const [name, setName] = useContext(Auth);
 console.log(name)

 return (...)};

Zwróć uwagę, że w komponencie funkcjonalnym Navbar() nie akceptujemy już żadnych rekwizytów. Zamiast tego używamy useContext(Auth) do wykorzystania kontekstu Auth, przechwytując zarówno nazwę, jak i setName. Oznacza to również, że nie musimy już przekazywać rekwizytów do paska nawigacyjnego:

./src/Aplikacja.jsx

// ... //
return (
   <BrowserRouter>
     <div className="h-screen">
       <Navbar/> // no need to pass prop anymore
// ... //

Aktualizacja kontekstu uwierzytelniania

Możemy również użyć udostępnionej funkcji setName do manipulowania zmienną stanu „name”:

./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");

Przedstawiamy hak useReducer().

W poprzednim przykładzie użyliśmy kontekstowego API do zarządzania i udostępniania naszego stanu w drzewie komponentów. Być może zauważyłeś, że nadal używamy useState jako podstawy naszej logiki i jest to w większości w porządku, ponieważ nasz stan jest nadal prostym obiektem w tym momencie, ale jeśli mielibyśmy rozszerzyć możliwości naszego procesu uwierzytelniania, z pewnością będziemy musieli przechowywać coś więcej niż tylko adres e-mail aktualnie zalogowanego użytkownika i w tym miejscu wracamy do ograniczeń, o których wspomnieliśmy wcześniej w odniesieniu do użycie useState ze stanem złożonym, na szczęście React rozwiązuje ten problem, udostępniając alternatywę dla useState do zarządzania stanem złożonym: wpisz useReducer.

useReducer można traktować jako uogólnioną wersję useState, przyjmuje ona dwa parametry: funkcję redukującą i stan początkowy.

Nie ma nic ciekawego do odnotowania w przypadku stanu początkowego, magia dzieje się wewnątrz funkcji reduktora: sprawdza ona rodzaj akcji, która miała miejsce i w zależności od tej akcji reduktor określi, jakie aktualizacje zastosować do stanu i zwróci nową wartość .

Patrząc na poniższy kod, funkcja reduktora ma dwa możliwe typy akcji:

  • LOGIN”: w takim przypadku stan użytkownika zostanie zaktualizowany o nowe dane uwierzytelniające użytkownika podane w ładunku akcji.

  • LOGOUT”: w takim przypadku stan użytkownika zostanie usunięty z pamięci lokalnej i przywrócony do wartości null.

Należy zauważyć, że obiekt akcji zawiera zarówno pole typu, które określa, jaką logikę zastosować, jak i opcjonalne pole ładunku, które dostarcza dane niezbędne do zastosowania tej logiki.

Na koniec hak useReducer zwraca bieżący stan i funkcję wysyłającą, której używamy do przekazania akcji do reduktora.

Reszta logiki jest identyczna jak w poprzednim przykładzie:

./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>
  );
};

Wysyłanie akcji zamiast używania funkcji ustawiającej setState – np.: setName –

Jak już wspomnieliśmy, używamy funkcji wysyłki, aby przekazać akcję do reduktora, w poniższym kodzie inicjujemy akcję LOGIN i podajemy adres e-mail użytkownika jako ładunek, teraz stan użytkownika zostanie zaktualizowany i ta zmiana zostanie wywołana ponowne renderowanie wszystkich komponentów subskrybowanych do stanu użytkownika. Należy zauważyć, że ponowne renderowanie zostanie uruchomione tylko wtedy, gdy nastąpi rzeczywista zmiana stanu, a ponowne renderowanie nie będzie możliwe, jeśli reduktor zwróci ten sam poprzedni stan.

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 (...)};

Login użytkownika

JWT Local storage

Profesjonalna wskazówka

Zwróć uwagę, że obiekt użytkownika, który otrzymujemy po udanym logowaniu, jest teraz przechowywany w localStorage.

Niestandardowy hak do logowania

Teraz, gdy już dobrze opanowaliśmy useReducer, możemy dalej hermetyzować naszą logikę logowania i wylogowywania w ich własnych, niestandardowych hakach, za pomocą jednego wywołania haka Login możemy obsłużyć wywołanie API do trasy logowania, pobrać nowego użytkownika poświadczenia i przechowuj je w pamięci lokalnej, wywołaj wywołanie LOGIN, aby zaktualizować stan użytkownika, a wszystko to przy obsłudze błędów:

./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 };
};

Uwaga: w przypadku bardziej zaawansowanych użytkowników React wśród czytelników, możesz zastanawiać się, dlaczego nie użyliśmy funkcji leniwej inicjalizacji useReducer do pobrania danych uwierzytelniających użytkownika. useReducer akceptuje trzeci opcjonalny parametr zwany funkcją init, ta funkcja jest używana w przypadku musimy zastosować pewną logikę, zanim będziemy mogli uzyskać początkową wartość stanu, powodem, dla którego nie zdecydowaliśmy się na to, jest prosta kwestia oddzielenia obaw, kod w ten sposób jest łatwiejszy do zrozumienia, a w rezultacie prostszy w utrzymaniu .

Strona logowania

Oto jak wygląda górna część naszej strony logowania po użyciu haka useLogin() w celu wyodrębnienia funkcji logowania i wywołaniu funkcji logowania z poświadczeniami przesłanymi przez użytkownika:

// ... ... //
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 (...)
// ... ... //

Wyłączamy także funkcję przesyłania, gdy użytkownik przesyła formularz:

<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>

I renderowanie wszelkich błędów uwierzytelniania, które otrzymujemy z naszego zaplecza:

{
  error && <span className="text-red-500 p-2">{error.message}</span>;
}

Rendering Errors

Utrzymanie stanu użytkownika

Być może zastanawiasz się, dlaczego musimy przechowywać obiekt użytkownika w localStorage, mówiąc najprościej, chcemy, aby użytkownik był zalogowany, dopóki token nie wygaśnie. Korzystanie z localStorage to świetny sposób na przechowywanie bitów JSON, tak jak w naszym przykładzie. Zwróć uwagę, jak stan zostaje wymazany, jeśli odświeżysz stronę po zalogowaniu. Można to łatwo rozwiązać za pomocą haka useEffect, aby sprawdzić, czy mamy przechowywany obiekt użytkownika w localStorage, jeśli taki istnieje, logujemy użytkownika automatycznie:

// ... ... //
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>
  );
};

Niestandardowe haki do wylogowania

To samo dotyczy haka Wyloguj, tutaj wysyłamy akcję LOGOUT, aby usunąć poświadczenia bieżącego użytkownika zarówno ze stanu, jak i z pamięci lokalnej:

./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 };
};

Wylogowanie użytkownika

Aby wylogować użytkownika, dodajmy zdarzenie kliknięcia do przycisku Wyloguj znajdującego się w UserDropdown.jsx i odpowiednio się nim obsłużmy:

./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>
// ... ... //

User Logout

Ochrona tras React

Ostatnim krokiem wdrożeniowym naszej aplikacji jest wykorzystanie globalnego stanu użytkownika do kontroli nawigacji użytkownika, szybkie przypomnienie jakie zachowanie powinniśmy osiągnąć: początkowo użytkownik witany jest stroną logowania, od tego momentu użytkownik może uzyskać dostęp tylko do strony głównej po udanym logowaniu, podobnie użytkownik zostanie przekierowany na stronę logowania po wylogowaniu.

Osiągamy to za pomocą biblioteki reagującej-router-dom, definiując 2 trasy: „/” i „/login”, kontrolujemy, który komponent ma być renderowany na każdej trasie, używając globalnego stanu uwierzytelnienia, auth mający wartość null reprezentuje nieuwierzytelnionego użytkownika i odwrotnie:

./src/Aplikacja.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;

Schemat podsumowania

Diagram

Zakończyć

W tym artykule próbowaliśmy zająć się prostym, ale bardzo powszechnym przypadkiem użycia zarządzania stanem w procesie uwierzytelniania, omawiając różne podejścia, uzasadnienie każdego z nich i związane z nimi kompromisy. Zarządzanie stanem w frameworkach po stronie klienta, a w szczególności w React, jest jednym z najczęściej omawianych tematów w społeczności frontendowej, po prostu dlatego, że może poprawić lub pogorszyć wydajność i skalowalność Twojej aplikacji. Sama ilość różnych technik, wzorców, bibliotek i narzędzi, które próbują rozwiązać ten problem zarządzania stanem, jest przytłaczająca. Naszym celem było zapewnienie solidnego zrozumienia praktyk, abyś mógł je wdrożyć we własnej aplikacji, w celu uzyskania bardziej zaawansowanych technik i wzorców zarządzania stanem w bardziej złożonych aplikacjach, sprawdź nasz kolejny artykuł, w którym zajmiemy się reduxem w celu skalowalnego zarządzania stanem w React.

Wkrótce

KontekstAPI a zestaw narzędzi Redux

Redux był akceptowany w społeczności przez wiele lat przed zestawem narzędzi Redux, w rzeczywistości RTK został wprowadzony jako szablon startowy do ładowania zarządzania stanem redux w nowych aplikacjach (jego pierwotna nazwa brzmiała „redux-starter-kit” w październiku 2019 r.), chociaż obecnie istnieje ogólna zgoda pomiędzy opiekunami Redux a społecznością co do tego, że zestaw narzędzi Redux jest właściwym sposobem pracy z Redux. RTX streszcza wiele logiki Redux, co ułatwia jego użycie, jest znacznie mniej gadatliwe i zachęca programistów do postępuj zgodnie z najlepszymi praktykami, jedną z głównych różnic między nimi jest to, że Redux został zbudowany tak, aby nie miał opinii, zapewniał minimalne API i oczekiwał, że programiści wykonają większość ciężkiej pracy, pisząc własne biblioteki do typowych zadań i zajmując się strukturą kodu, skutkowało to powolnym czasem programowania i bałaganem w kodzie. Dodano zestaw narzędzi Redux jako dodatkową warstwę abstrakcji, która zapobiega wpadnięciu programistów w typowe pułapki. Więcej informacji i argumentów opiekunów można znaleźć w oficjalnej dokumentacji tutaj.


Career Services background pattern

Usługi związane z karierą

Contact Section background image

Pozostańmy w kontakcie

Code Labs Academy © 2025 Wszelkie prawa zastrzeżone.