Quản lý trạng thái trong React: Một ví dụ thực tế

Cập nhật trên September 03, 2024 26 phút Đọc

Quản lý trạng thái trong React: Một ví dụ thực tế cover image

React là framework phù hợp để xây dựng các ứng dụng động phía máy khách cho nhiều nhà phát triển. Bản chất năng động của các ứng dụng này xuất phát từ tính linh hoạt và danh sách mở rộng các khả năng cũng như tính năng có thể có ở phía máy khách, cho phép các nhà phát triển xây dựng các ứng dụng hoàn chỉnh tải trên trình duyệt chỉ trong vài giây, một kỳ tích chưa từng có. có thể (hoặc rất cồng kềnh) trong thời của web tĩnh.

Với sự mở rộng về khả năng này, khái niệm quản lý trạng thái xuất hiện, khi độ phức tạp tăng lên trong các ứng dụng phía máy khách, nhu cầu giữ trạng thái cục bộ sẽ trở thành nút thắt cổ chai nếu không được xử lý chính xác và lưu ý đến khả năng mở rộng.

Vấn đề này đã được giải quyết bằng nhiều khung, tuân theo các cách tiếp cận khác nhau và tập trung vào các nhóm vấn đề phụ khác nhau. Đó là lý do tại sao điều quan trọng là phải có hiểu biết sâu sắc về hệ sinh thái của khung lựa chọn để đánh giá nhu cầu của từng ứng dụng và sử dụng cách tiếp cận phù hợp theo những điều đó. số liệu. Bài viết này sẽ cung cấp cho bạn cái nhìn tổng quan ngắn gọn về các vấn đề quản lý trạng thái phổ biến và cố gắng giới thiệu các cách tiếp cận khác nhau (useState, Context API) để giải quyết vấn đề đó. Mặc dù bài viết này sẽ trình bày nhiều giải pháp nhưng nó sẽ chỉ tập trung vào những thách thức ở quy mô nhỏ hơn, chúng tôi sẽ đề cập đến các chủ đề nâng cao hơn trong các bài viết sắp tới.

Quy trình xác thực

Bạn có thể tìm thấy mã được giới thiệu trong suốt bài viết tại đây.

Bạn có thể truy cập liên kết xem trước trực tiếp tại đây.

Hãy xem xét trường hợp chúng tôi triển khai quy trình xác thực cho ứng dụng React.

User Login

Như được hiển thị trong GIF ở trên, chúng tôi muốn cho phép người dùng đăng nhập hoặc đăng ký vào ứng dụng của chúng tôi bằng thông tin xác thực. Nếu thông tin xác thực hợp lệ được cung cấp, người dùng sẽ đăng nhập, ứng dụng sẽ tự động điều hướng đến trang chủ và người dùng có thể tiếp tục sử dụng ứng dụng.

Tương tự, nếu người dùng đăng xuất, tài nguyên trang chủ sẽ được bảo vệ sau khi đăng nhập, trang đăng nhập sẽ là trang duy nhất mà người dùng có thể truy cập.

Nghĩ về quy trình làm việc này về mặt triển khai, chúng ta sẽ có một thành phần chính có tên là Ứng dụng, thành phần Ứng dụng sẽ chuyển hướng người dùng đến một trong hai trang: Trang chủ hoặc Đăng nhập, trạng thái hiện tại của người dùng (đăng nhập, đăng xuất) sẽ quyết định trang nào trang mà người dùng được chuyển hướng đến, một thay đổi về trạng thái hiện tại của người dùng (ví dụ: thay đổi từ đăng nhập sang đăng xuất) sẽ kích hoạt chuyển hướng ngay lập tức đến trang tương ứng.

State

Như được hiển thị trong hình minh họa ở trên, chúng tôi muốn thành phần Ứng dụng xem xét trạng thái hiện tại và chỉ hiển thị một trong hai trang – Trang chủ hoặc Đăng nhập – dựa trên trạng thái hiện tại đó.

Nếu người dùng là null, điều đó có nghĩa là chúng tôi không có bất kỳ người dùng được xác thực nào, vì vậy chúng tôi tự động điều hướng đến trang đăng nhập và bảo vệ trang chủ bằng cách sử dụng kết xuất có điều kiện. Nếu người dùng tồn tại, chúng tôi làm ngược lại.

Bây giờ chúng ta đã hiểu rõ về những gì nên triển khai, hãy khám phá một vài tùy chọn, nhưng trước tiên hãy thiết lập dự án React của chúng ta,

Kho dự án chứa một ứng dụng phụ trợ mẫu mà chúng tôi sẽ sử dụng để triển khai giao diện người dùng (chúng tôi sẽ không đi sâu vào nó vì đây không phải là trọng tâm chính ở đây nhưng mã được cố tình giữ đơn giản để bạn không gặp khó khăn với nó )

Chúng tôi bắt đầu bằng cách tạo các trang và thành phần sau:

  • Trang chủ

  • Trang đăng nhập

  • Trang chỉnh sửa người dùng

  • Thành phần thanh điều hướng

  • Thành phần thả xuống của người dùng

Một ứng dụng React có nhiều trang cần điều hướng thích hợp, để làm được điều đó, chúng ta có thể sử dụng Reac-router-dom để tạo bối cảnh bộ định tuyến trình duyệt toàn cầu và đăng ký các tuyến React khác nhau.


yarn add react-router-dom

Chúng tôi biết nhiều độc giả thích làm theo cùng với hướng dẫn, vì vậy đây là mẫu khởi đầu để giúp bạn bắt kịp tốc độ. Nhánh khởi đầu này sử dụng DaisyUI cho các thành phần JSX TailwindCSS được xác định trước. Nó bao gồm tất cả các thành phần, trang và bộ định tuyến đã được thiết lập. Nếu bạn đang cân nhắc làm theo hướng dẫn này, tự mình xây dựng toàn bộ quy trình xác thực theo các bước đơn giản, trước tiên hãy bắt đầu bằng cách phân nhánh kho lưu trữ. Sau khi bạn phân nhánh kho lưu trữ, hãy sao chép kho lưu trữ đó và bắt đầu từ start-herebranch:


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

Khi bạn kéo nhánh bắt đầu tại đây:

  • Mở dự án bằng trình soạn thảo mã ưa thích của bạn

  • Thay đổi thư mục thành frontend/

  • Cài đặt phụ thuộc: sợi

  • Khởi động máy chủ phát triển: sợi dev ·

Bản xem trước sẽ trông giống như thế này:

Changing user name

Tên được hiển thị trong Thanh điều hướng – phía trên bên phải – là một biến trạng thái được xác định trong thành phần Ứng dụng chính. Biến tương tự được chuyển tới cả Thanh điều hướng và Trang chủ. Biểu mẫu đơn giản được sử dụng ở trên thực sự cập nhật biến trạng thái “name” từ thành phần EditPage.

Hai cách tiếp cận được trình bày dưới đây sẽ đi sâu vào chi tiết thực hiện:

Cách tiếp cận đầu tiên: useState

useState()

useState là một trong những React hook được sử dụng phổ biến nhất, nó cho phép bạn tạo và thay đổi trạng thái trong thành phần React Function. Thành phần useState có cách triển khai thực sự đơn giản và dễ sử dụng: để tạo một trạng thái mới, bạn cần gọi useState với giá trị ban đầu của trạng thái đó và hook useState sẽ trả về một mảng chứa hai biến: biến đầu tiên là trạng thái biến mà bạn có thể sử dụng để tham chiếu trạng thái của mình và biến thứ hai là hàm mà bạn sử dụng để thay đổi giá trị của trạng thái: khá đơn giản.

Thế còn chúng ta thấy điều đó trong thực tế thì sao? Tên được hiển thị trong Thanh điều hướng – phía trên bên phải – là một biến trạng thái được xác định trong thành phần Ứng dụng chính. Biến tương tự được chuyển tới cả Thanh điều hướng và Trang chủ. Biểu mẫu đơn giản được sử dụng ở trên thực sự cập nhật biến trạng thái “name” từ thành phần EditPage. Điểm mấu chốt là thế này: useState là một hook lõi chấp nhận trạng thái ban đầu làm tham số và trả về hai biến chứa hai giá trị, biến trạng thái chứa trạng thái ban đầu và hàm setter cho cùng một biến trạng thái.

Hãy chia nhỏ nó ra và xem cách nó được triển khai ngay từ đầu.

  1. Tạo biến trạng thái “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 (...)};

đạo cụ

Về mặt khái niệm, đạo cụ là một trong những khối xây dựng cơ bản của thành phần phản ứng, nếu bạn coi thành phần chức năng React là một hàm Javascript, thì đạo cụ không hơn gì các tham số của hàm, việc kết hợp đạo cụ và hook useState có thể cung cấp cho bạn một khuôn khổ vững chắc để quản lý trạng thái trên một ứng dụng React đơn giản.

Đạo cụ phản ứng được chuyển dưới dạng thuộc tính cho các thành phần tùy chỉnh. Các thuộc tính được truyền dưới dạng props có thể bị hủy cấu trúc khỏi đối tượng props khi chấp nhận nó làm đối số, tương tự như sau:

Truyền đạo cụ

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

Đạo cụ có thể được chấp nhận và sử dụng bên trong một thành phần chức năng tương tự như các đối số hàm thông thường. Đó là vì “name” được truyền dưới dạng prop cho thành phần Home nên chúng ta có thể hiển thị nó trong cùng một thành phần. Trong ví dụ sau, chúng ta chấp nhận prop được truyền bằng cú pháp phá hủy để trích xuất thuộc tính name từ đối tượng props.

Nhận đạo cụ

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

Mẹo chuyên nghiệp

Mở bảng điều khiển của trình duyệt và chú ý cách tất cả các thành phần sử dụng prop “name” hiển thị lại khi trạng thái thay đổi. Khi thao tác với một biến trạng thái, React sẽ lưu trữ trạng thái tiếp theo, hiển thị lại thành phần của bạn với các giá trị mới và cập nhật giao diện người dùng.

Components Re-rendering

Hạn chế của useState

Đạo cụ-Khoan

Khoan đạo cụ là một thuật ngữ để chỉ hệ thống phân cấp của các thành phần trong đó một tập hợp các thành phần cần một số đạo cụ nhất định do thành phần gốc cung cấp, một cách giải quyết phổ biến mà nhà phát triển thiếu kinh nghiệm thường sử dụng là chuyển các đạo cụ này trong toàn bộ chuỗi thành phần, vấn đề với điều này Cách tiếp cận là một sự thay đổi trong bất kỳ đạo cụ nào trong số này sẽ kích hoạt toàn bộ chuỗi thành phần kết xuất lại, làm chậm toàn bộ ứng dụng một cách hiệu quả do những kết xuất không cần thiết này, các thành phần ở giữa chuỗi không yêu cầu những đạo cụ này đóng vai trò là phương tiện để chuyển đạo cụ.

Ví dụ:

  • Một biến trạng thái đã được xác định trong thành phần Ứng dụng chính bằng cách sử dụng hook useState()

  • Một prop tên đã được chuyển tới thành phần Navbar

  • Prop tương tự được chấp nhận trong Navbar và được chuyển dưới dạng prop một lần nữa cho thành phần UserDropdown

  • UserDropdown là phần tử con cuối cùng chấp nhận prop.

Prop-drilling

./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/thành phần/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>
    </>
  );
}

Hãy tưởng tượng việc duy trì một ứng dụng React với các lớp thành phần khác nhau đều chuyển và hiển thị cùng một trạng thái khó đến mức nào.

Độ phức tạp ngày càng tăng và chất lượng mã

Với việc sử dụng useState và props làm phương tiện quản lý trạng thái duy nhất trong ứng dụng phản ứng, cơ sở mã có thể nhanh chóng trở nên phức tạp, phải quản lý hàng chục hoặc hàng trăm biến trạng thái, có thể trùng lặp với nhau, nằm rải rác trên các tệp khác nhau và các thành phần có thể khá khó khăn, bất kỳ thay đổi nào đối với một biến trạng thái nhất định sẽ yêu cầu xem xét cẩn thận sự phụ thuộc giữa các thành phần để tránh bất kỳ khả năng hiển thị lại bổ sung nào trong một ứng dụng vốn đã chậm.

Cách tiếp cận thứ hai: API ngữ cảnh

API ngữ cảnh là nỗ lực của React nhằm giải quyết những hạn chế của việc chỉ sử dụng props và useState để quản lý trạng thái, đặc biệt, API ngữ cảnh là câu trả lời cho vấn đề đã đề cập trước đó về nhu cầu truyền props xuống toàn bộ cây thành phần. Với việc sử dụng ngữ cảnh, bạn có thể xác định trạng thái cho dữ liệu mà bạn cho là toàn cục và truy cập trạng thái của nó từ bất kỳ điểm nào trong cây thành phần: không cần khoan lỗ nữa.

Điều quan trọng cần chỉ ra là API ngữ cảnh ban đầu được hình thành để giải quyết vấn đề chia sẻ dữ liệu toàn cầu, dữ liệu như chủ đề giao diện người dùng, thông tin xác thực (trường hợp sử dụng, ngôn ngữ, v.v.) của chúng tôi), đối với các loại dữ liệu khác cần được chia sẻ trong số nhiều thành phần nhưng không nhất thiết phải mang tính toàn cầu cho tất cả ứng dụng, ngữ cảnh có thể không phải là lựa chọn tốt nhất, tùy thuộc vào trường hợp sử dụng, bạn có thể xem xét các kỹ thuật khác như thành phần thành phần nằm ngoài phạm vi của bài viết này.

Chuyển sang ngữ cảnh

Tạo bối cảnh xác thực

createContext rất đơn giản, nó tạo ra một biến ngữ cảnh mới, nó nhận vào một tham số tùy chọn duy nhất: giá trị mặc định của biến ngữ cảnh.

Hãy xem cách thực hiện này, trước tiên hãy tạo một thư mục mới “contexts” và một tệp mới bên trong nó là “Auth.jsx”. Để tạo ngữ cảnh mới, chúng ta cần gọi hàm createContext(), gán giá trị trả về cho biến Auth mới sẽ được xuất tiếp theo:

./src/contexts/Auth.jsx

import { createContext } from "react";

export const Auth = createContext();

Cung cấp bối cảnh xác thực

Bây giờ chúng ta cần hiển thị biến ngữ cảnh “Auth” mà chúng ta đã tạo trước đó, để đạt được điều này, chúng ta sử dụng trình cung cấp ngữ cảnh, trình cung cấp ngữ cảnh là một thành phần có prop “value”, chúng ta có thể sử dụng prop này để chia sẻ một giá trị – tên – trên cây thành phần, bằng cách gói cây thành phần với thành phần nhà cung cấp, chúng tôi làm cho giá trị đó có thể truy cập được từ bất kỳ đâu bên trong cây thành phần mà không cần phải truyền giá trị đó cho từng thành phần con riêng lẻ.

Trong ví dụ xác thực của chúng tôi, chúng tôi cần một biến để giữ đối tượng người dùng và một số loại setter để thao tác biến trạng thái đó, đây là trường hợp sử dụng hoàn hảo cho useState. Khi sử dụng Ngữ cảnh, bạn cần đảm bảo rằng bạn đang xác định dữ liệu bạn muốn cung cấp và chuyển dữ liệu đó – người dùng trong ví dụ của chúng tôi – tới tất cả các cây thành phần được lồng bên trong, vì vậy hãy xác định một biến trạng thái mới user bên trong thành phần nhà cung cấp và cuối cùng chúng ta chuyển cả user và setUser vào trong một mảng làm giá trị mà nhà cung cấp sẽ hiển thị cho cây thành phần:

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

Một điều khác mà chúng ta có thể làm là di chuyển biến trạng thái “name” từ thành phần ứng dụng chính sang bối cảnh Auth và hiển thị nó cho các thành phần lồng nhau:

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

Bây giờ tất cả những gì còn lại là lồng ứng dụng của chúng ta vào cùng một thành phần AuthProvider mà chúng ta vừa xuất.

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

Vì chúng tôi đang hiển thị prop con bên trong Auth.Provider nên tất cả các phần tử được lồng bên trong thành phần AuthProvider hiện có thể sử dụng prop value mà chúng tôi đã chuyển cho Auth.Provider. Điều này có vẻ khó hiểu, nhưng một khi bạn thử nghiệm nó, hãy thử cung cấp và sử dụng trạng thái - Ngữ cảnh - toàn cầu. Rốt cuộc, nó chỉ có ý nghĩa với tôi sau khi thử nghiệm API ngữ cảnh.

Sử dụng bối cảnh xác thực

Bước cuối cùng rất đơn giản, chúng ta sử dụng móc ngữ cảnh “useContext” để truy cập giá trị mà nhà cung cấp ngữ cảnh của “Auth” đang cung cấp, trong trường hợp của chúng ta là mảng chứa người dùng và setUser, trong đoạn mã sau, chúng ta có thể sử dụng useContext để sử dụng bối cảnh Auth bên trong Thanh điều hướng. Điều này chỉ có thể thực hiện được vì Navbar được lồng bên trong thành phần Ứng dụng và vì AuthProvider bao quanh thành phần Ứng dụng nên giá trị prop chỉ có thể được sử dụng bằng cách sử dụng hook useContext. Một công cụ tuyệt vời khác mà React cung cấp ngay lập tức để quản lý mọi dữ liệu có thể được truy cập trên toàn cầu và cũng có thể bị thao túng bởi bất kỳ thành phần tiêu dùng nào.

./src/thành phần/Navbar.jsx

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

 return (...)};

Lưu ý rằng chúng tôi không chấp nhận bất kỳ props nào nữa trong thành phần chức năng Navbar(). Thay vào đó, chúng tôi đang sử dụng useContext(Auth) để sử dụng bối cảnh Auth, lấy cả tên và setName. Điều này cũng có nghĩa là chúng ta không cần truyền prop vào Navbar nữa:

./src/App.jsx

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

Cập nhật bối cảnh xác thực

Chúng ta cũng có thể sử dụng hàm setName được cung cấp để thao tác với biến trạng thái “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");

Giới thiệu hook useReducer()

Trong ví dụ trước, chúng tôi đã sử dụng API ngữ cảnh để quản lý và chia sẻ trạng thái của mình trong cây thành phần, bạn có thể nhận thấy rằng chúng tôi vẫn đang sử dụng useState làm cơ sở logic và điều này hầu như ổn vì trạng thái của chúng tôi vẫn là một đối tượng đơn giản tại thời điểm này, nhưng nếu chúng tôi muốn mở rộng khả năng của luồng xác thực, chúng tôi chắc chắn sẽ cần lưu trữ nhiều thứ hơn là chỉ email của người dùng hiện đang đăng nhập và đây là lúc chúng tôi quay trở lại những hạn chế mà chúng tôi đã đề cập trước đây liên quan đến May mắn thay, việc sử dụng useState với trạng thái phức tạp, React giải quyết vấn đề này bằng cách cung cấp một giải pháp thay thế cho useState để quản lý trạng thái phức tạp: nhập useReducer.

useReducer có thể được coi là một phiên bản tổng quát của useState, nó có hai tham số: hàm rút gọn và trạng thái ban đầu.

Không có gì thú vị cần lưu ý đối với trạng thái ban đầu, điều kỳ diệu xảy ra bên trong hàm giảm tốc: nó kiểm tra loại hành động đã xảy ra và tùy thuộc vào hành động đó, bộ giảm tốc sẽ xác định những cập nhật nào sẽ áp dụng cho trạng thái và trả về giá trị mới của nó .

Nhìn vào đoạn mã bên dưới, hàm giảm tốc có hai loại hành động có thể thực hiện:

  • LOGIN”: trong trường hợp đó, trạng thái người dùng sẽ được cập nhật bằng thông tin xác thực người dùng mới được cung cấp bên trong tải trọng hành động.

  • LOGOUT”: trong trường hợp đó trạng thái người dùng sẽ bị xóa khỏi bộ nhớ cục bộ và được đặt về giá trị rỗng.

Điều quan trọng cần lưu ý là đối tượng hành động chứa cả trường loại xác định logic nào sẽ áp dụng và trường tải trọng tùy chọn để cung cấp dữ liệu cần thiết cho việc áp dụng logic đó.

Cuối cùng, hook useReducer trả về trạng thái hiện tại và một hàm điều phối mà chúng ta sử dụng để chuyển một hành động tới bộ giảm tốc.

Đối với phần còn lại của logic, nó giống với ví dụ trước:

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

Gửi các hành động thay vì sử dụng hàm setter setState – ví dụ: setName –

Như chúng tôi vừa đề cập, chúng tôi sử dụng chức năng điều phối để chuyển một hành động tới bộ giảm tốc, trong đoạn mã sau, chúng tôi bắt đầu một hành động ĐĂNG NHẬP và chúng tôi cung cấp email người dùng dưới dạng tải trọng, bây giờ trạng thái người dùng sẽ được cập nhật và thay đổi này sẽ kích hoạt kết xuất lại tất cả các thành phần đã đăng ký trạng thái người dùng. Điều quan trọng cần chỉ ra là kết xuất lại sẽ chỉ được kích hoạt nếu xảy ra thay đổi thực tế về trạng thái, không hiển thị lại nếu bộ giảm tốc trả về cùng trạng thái trước đó.

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

Đăng nhập người dùng

JWT Local storage

Mẹo chuyên nghiệp

Lưu ý cách đối tượng người dùng mà chúng tôi nhận được sau khi đăng nhập thành công hiện được lưu trữ trong localStorage.

Móc tùy chỉnh để đăng nhập

Bây giờ chúng ta đã nắm rõ useReducer, chúng ta có thể gói gọn logic đăng nhập và đăng xuất của mình vào các móc tùy chỉnh riêng biệt của riêng chúng, bằng một lệnh gọi đến móc Đăng nhập, chúng ta có thể xử lý lệnh gọi API đến lộ trình đăng nhập, truy xuất người dùng mới thông tin xác thực và lưu trữ chúng trong bộ nhớ cục bộ, gửi lệnh gọi LOGIN để cập nhật trạng thái người dùng, đồng thời xử lý việc xử lý lỗi:

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

Lưu ý: đối với những người đọc React nâng cao hơn, bạn có thể thắc mắc tại sao chúng tôi không sử dụng tính năng khởi tạo lười biếng của useReducer để truy xuất thông tin xác thực của người dùng, useReducer chấp nhận tham số tùy chọn thứ ba được gọi là hàm init, hàm này được sử dụng trong trường hợp chúng ta cần áp dụng một số logic trước khi có thể nhận được giá trị ban đầu của trạng thái, lý do chúng ta không chọn điều này là một vấn đề đơn giản về việc phân tách các mối quan tâm, mã theo cách này dễ hiểu hơn và do đó dễ bảo trì hơn .

Trang đăng nhập

Đây là phần trên cùng của trang Đăng nhập của chúng tôi trông như thế nào sau khi sử dụng hook useLogin() để trích xuất chức năng đăng nhập và gọi chức năng đăng nhập bằng thông tin đăng nhập do người dùng gửi:

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

Chúng tôi cũng đang tắt chức năng Gửi khi người dùng gửi biểu mẫu:

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

Và hiển thị bất kỳ lỗi xác thực nào chúng tôi nhận được từ chương trình phụ trợ của mình:

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

Rendering Errors

Duy trì trạng thái người dùng

Bạn có thể thắc mắc tại sao chúng tôi cần lưu trữ đối tượng người dùng trong localStorage, nói một cách đơn giản, chúng tôi muốn duy trì trạng thái đăng nhập của người dùng miễn là mã thông báo chưa hết hạn. Sử dụng localStorage là một cách tuyệt vời để lưu trữ các bit JSON, giống như trong ví dụ của chúng tôi. Lưu ý cách trạng thái bị xóa nếu bạn làm mới trang sau khi đăng nhập. Điều này có thể được giải quyết dễ dàng bằng cách sử dụng hook useEffect để kiểm tra xem chúng tôi có đối tượng người dùng được lưu trữ trong localStorage hay không, nếu có, chúng tôi sẽ tự động đăng nhập người dùng:

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

Móc tùy chỉnh để đăng xuất

Điều tương tự cũng áp dụng với hook Logout, ở đây chúng tôi sẽ gửi một hành động LOGOUT để xóa thông tin đăng nhập của người dùng hiện tại khỏi cả trạng thái và bộ nhớ cục bộ:

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

Người dùng đăng xuất

Để đăng xuất người dùng, hãy thêm một sự kiện nhấp chuột vào nút Đăng xuất được tìm thấy trong UserDropdown.jsx và xử lý nó cho phù hợp:

./src/thành phần/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

Bảo vệ đường phản ứng

Bước cuối cùng trong việc triển khai ứng dụng của chúng tôi là tận dụng trạng thái người dùng toàn cầu để kiểm soát việc điều hướng của người dùng, một lời nhắc nhở nhanh về hành vi mà chúng tôi nên đạt được: ban đầu người dùng được chào đón bởi trang đăng nhập, từ thời điểm đó người dùng chỉ có thể truy cập trang chủ sau khi đăng nhập thành công, tương tự người dùng sẽ được chuyển hướng đến trang đăng nhập khi đăng xuất.

Chúng tôi đạt được điều này với sự trợ giúp của thư viện Reac-router-dom bằng cách xác định 2 tuyến: ”/” và “/login”, chúng tôi kiểm soát thành phần nào sẽ hiển thị tại mỗi tuyến bằng trạng thái xác thực toàn cục, auth đánh giá thành null đại diện cho người dùng chưa được xác thực và ngược lại:

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

Sơ đồ tóm tắt

Diagram

Tóm tắt

Trong bài viết này, chúng tôi đã cố gắng giải quyết trường hợp sử dụng đơn giản nhưng rất phổ biến trong việc triển khai quản lý trạng thái cho quy trình xác thực, xem xét các cách tiếp cận khác nhau, lý do đằng sau mỗi cách và sự cân bằng của chúng. Quản lý trạng thái trong các khung phía máy khách và đặc biệt là trong React là một trong những chủ đề được nhắc đến nhiều nhất trong cộng đồng giao diện người dùng, đơn giản vì nó có thể tạo ra hoặc phá vỡ hiệu suất và khả năng mở rộng của ứng dụng của bạn. Số lượng lớn các kỹ thuật, mẫu, thư viện và công cụ khác nhau cố gắng giải quyết vấn đề quản lý trạng thái này là quá nhiều, mục tiêu của chúng tôi là cung cấp cho bạn sự hiểu biết vững chắc về các phương pháp thực hành để bạn có thể triển khai nó trong ứng dụng của riêng mình, nâng cao hơn các kỹ thuật và mô hình quản lý trạng thái trong các ứng dụng phức tạp hơn, hãy xem bài viết tiếp theo của chúng tôi, nơi chúng tôi đề cập đến redux để quản lý trạng thái có thể mở rộng trong React.

Sắp ra mắt

ContextAPI và Redux Toolkit

Redux đã được áp dụng trong cộng đồng nhiều năm trước khi có bộ công cụ Redux, trên thực tế, RTK đã được giới thiệu như một mẫu khởi đầu để khởi động quản lý trạng thái redux trong các ứng dụng mới (tên ban đầu của nó là “redux-starter-kit” vào tháng 10 năm 2019), mặc dù ngày nay, có sự đồng thuận chung giữa những người bảo trì Redux và cộng đồng rằng bộ công cụ Redux là cách hợp lệ để làm việc với redux.RTX tóm tắt rất nhiều logic Redux giúp sử dụng dễ dàng hơn, ít dài dòng hơn và khuyến khích các nhà phát triển tuân theo các phương pháp hay nhất, một điểm khác biệt chính giữa cả hai là Redux được xây dựng theo hướng không cố định, cung cấp API tối thiểu và mong muốn các nhà phát triển thực hiện hầu hết các công việc nặng nhọc bằng cách viết thư viện của riêng họ cho các tác vụ thông thường và xử lý cấu trúc mã, điều này dẫn đến thời gian phát triển chậm và mã lộn xộn, bộ công cụ Redux đã được thêm vào như một lớp trừu tượng bổ sung nhằm ngăn cản các nhà phát triển rơi vào những cạm bẫy phổ biến, hãy tham khảo tài liệu chính thức để biết thêm thông tin chi tiết và lý do từ những người bảo trì tại đây.