การจัดการสถานะในการตอบสนอง: ตัวอย่างเชิงปฏิบัติ

อัปเดตบน September 03, 2024 8 นาทีอ่าน

การจัดการสถานะในการตอบสนอง: ตัวอย่างเชิงปฏิบัติ cover image

React เป็นเฟรมเวิร์ก go-to สำหรับการสร้างแอปพลิเคชันฝั่งไคลเอ็นต์แบบไดนามิกสำหรับนักพัฒนาจำนวนมาก ลักษณะแบบไดนามิกของแอปพลิเคชันเหล่านี้มาจากความยืดหยุ่นและรายการความสามารถและคุณลักษณะเพิ่มเติมที่เป็นไปได้ในฝั่งไคลเอ็นต์ ซึ่งช่วยให้นักพัฒนาสามารถสร้างแอปพลิเคชันที่ครบครันโดยโหลดบนเบราว์เซอร์ในเวลาไม่กี่วินาที ซึ่งเป็นความสำเร็จที่ไม่ได้ เป็นไปได้ (หรือยุ่งยากมาก) ในยุคของเว็บแบบคงที่

ด้วยการขยายความเป็นไปได้นี้ แนวคิดในการจัดการสถานะก็เกิดขึ้น เมื่อความซับซ้อนเพิ่มขึ้นในแอปพลิเคชันฝั่งไคลเอ็นต์ ความจำเป็นในการรักษาสถานะท้องถิ่นก็จะกลายเป็นปัญหาคอขวดในตัวเองหากไม่ได้รับการจัดการอย่างถูกต้องและคำนึงถึงความสามารถในการปรับขนาดได้

ปัญหานี้ได้รับการแก้ไขด้วยกรอบงานจำนวนมาก ตามแนวทางที่แตกต่างกันและมุ่งเน้นไปที่ชุดปัญหาย่อยที่แตกต่างกัน นั่นคือเหตุผลว่าทำไมการมีความเข้าใจในระดับสูงเกี่ยวกับระบบนิเวศของกรอบงานที่เลือกเพื่อประเมินความต้องการของแต่ละแอปพลิเคชัน และใช้แนวทางที่ถูกต้องตามสิ่งเหล่านั้น เมตริก บทความนี้จะให้ภาพรวมโดยย่อเกี่ยวกับปัญหาการจัดการสถานะทั่วไป และพยายามแนะนำแนวทางต่างๆ (useState, Context API) เพื่อตอบสนองต่อปัญหาดังกล่าว แม้ว่าบทความนี้จะนำเสนอวิธีแก้ปัญหาหลายประการ แต่จะเน้นไปที่ความท้าทายในระดับที่เล็กลงเท่านั้น แต่เราจะกล่าวถึงหัวข้อขั้นสูงเพิ่มเติมในบทความต่อๆ ไป

ขั้นตอนการตรวจสอบสิทธิ์

ดูโค้ดที่แสดงทั่วทั้งบทความได้ที่นี่.

คุณสามารถเข้าถึงลิงก์ตัวอย่างสดได้ที่นี่.

พิจารณากรณีที่เราใช้กระบวนการตรวจสอบสิทธิ์สำหรับแอปพลิเคชัน React

User Login

ดังที่แสดงใน GIF ด้านบน เราต้องการให้ผู้ใช้สามารถเข้าสู่ระบบหรือสมัครใช้งานแอปพลิเคชันของเราโดยใช้ข้อมูลประจำตัว หากระบุข้อมูลรับรองที่ถูกต้อง ผู้ใช้จะเข้าสู่ระบบ แอปพลิเคชันจะนำทางไปยังหน้าแรกโดยอัตโนมัติ และผู้ใช้สามารถใช้แอปพลิเคชันต่อไปได้

ในทำนองเดียวกัน หากผู้ใช้ออกจากระบบ ทรัพยากรของหน้าแรกจะได้รับการคุ้มครองหลังการเข้าสู่ระบบ หน้าเข้าสู่ระบบจะเป็นหน้าเดียวที่ผู้ใช้สามารถเข้าถึงได้

เมื่อคิดถึงเวิร์กโฟลว์นี้ในแง่ของการนำไปปฏิบัติ เราจะมีส่วนประกอบหลักชื่อ App ส่วนประกอบ App จะเปลี่ยนเส้นทางผู้ใช้ไปยังหน้าใดหน้าหนึ่งจากสองหน้า: หน้าแรกหรือเข้าสู่ระบบ สถานะปัจจุบันของผู้ใช้ (เข้าสู่ระบบ ออกจากระบบ) จะกำหนดว่า หน้าที่ผู้ใช้ถูกเปลี่ยนเส้นทางไป การเปลี่ยนแปลงสถานะปัจจุบันของผู้ใช้ (เช่น การเปลี่ยนจากการเข้าสู่ระบบเป็นออกจากระบบ) ควรกระตุ้นให้เกิดการเปลี่ยนเส้นทางทันทีไปยังหน้าเว็บที่เกี่ยวข้อง

State

ดังที่แสดงในภาพประกอบด้านบน เราต้องการให้ส่วนประกอบแอปพิจารณาสถานะปัจจุบันและแสดงผลเพียงหนึ่งในสองเพจ - หน้าแรกหรือเข้าสู่ระบบ - ตามสถานะปัจจุบันนั้น

หากผู้ใช้เป็นค่าว่าง หมายความว่าเราไม่มีผู้ใช้ที่ได้รับการรับรองความถูกต้อง ดังนั้นเราจึงไปที่หน้าเข้าสู่ระบบโดยอัตโนมัติและปกป้องหน้าแรกโดยใช้การแสดงผลแบบมีเงื่อนไข หากมีผู้ใช้อยู่ เราก็ทำตรงกันข้าม

ตอนนี้เรามีความเข้าใจอย่างมั่นคงเกี่ยวกับสิ่งที่ควรนำไปใช้แล้ว เรามาสำรวจตัวเลือกสองสามตัวกัน แต่ก่อนอื่นเรามาตั้งค่าโปรเจ็กต์ React ของเราก่อน

repo ของโปรเจ็กต์ประกอบด้วยแอปพลิเคชันแบ็กเอนด์ตัวอย่างที่เราจะใช้เพื่อใช้งานส่วนหน้า (เราจะไม่พูดถึงมันเนื่องจากนี่ไม่ใช่จุดสนใจหลักที่นี่ แต่โค้ดถูกเก็บไว้อย่างเรียบง่ายโดยเจตนา ดังนั้นคุณจะไม่มีปัญหากับมัน )

เราเริ่มต้นด้วยการสร้างเพจและส่วนประกอบต่อไปนี้:

  • หน้าแรก

  • หน้าเข้าสู่ระบบ

  • แก้ไขหน้าผู้ใช้

  • ส่วนประกอบ Navbar

  • ส่วนประกอบ UserDropdown

แอปพลิเคชัน React ที่มีหลายเพจจำเป็นต้องมีการนำทางที่เหมาะสม เพื่อที่เราจะได้ใช้ react-router-dom เพื่อสร้างบริบทเราเตอร์ของเบราว์เซอร์ส่วนกลาง และลงทะเบียนเส้นทาง React ที่แตกต่างกัน


yarn add react-router-dom

เรารู้ว่าผู้อ่านจำนวนมากชอบติดตามพร้อมกับบทช่วยสอน ดังนั้นนี่คือเทมเพลตเริ่มต้นที่จะช่วยให้คุณได้รับข้อมูลที่รวดเร็ว สาขาเริ่มต้นนี้ใช้ DaisyUI สำหรับส่วนประกอบ TailwindCSS JSX ที่กำหนดไว้ล่วงหน้า ประกอบด้วยส่วนประกอบ หน้า และเราเตอร์ทั้งหมดที่ติดตั้งไว้แล้ว หากคุณกำลังพิจารณาที่จะปฏิบัติตามบทช่วยสอนนี้ โดยสร้างโฟลว์การตรวจสอบสิทธิ์ทั้งหมดด้วยตัวเองโดยทำตามขั้นตอนที่ไม่ซับซ้อน ให้เริ่มด้วยการฟอร์กที่เก็บข้อมูลก่อน หลังจากที่คุณแยกที่เก็บแล้ว ให้โคลนและเริ่มจาก เริ่มต้นที่นี่branch:


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

เมื่อคุณดึงสาขา start-here:

  • เปิดโครงการด้วยโปรแกรมแก้ไขโค้ดที่คุณต้องการ

  • เปลี่ยนไดเร็กทอรีเป็น ส่วนหน้า/

  • ติดตั้งการพึ่งพา: เส้นด้าย

  • เริ่มเซิร์ฟเวอร์การพัฒนา: Yarn dev·

การแสดงตัวอย่างควรมีลักษณะดังนี้:

Changing user name

ชื่อที่แสดงผลใน Navbar - ด้านขวาบน - เป็นตัวแปรสถานะที่กำหนดไว้ในส่วนประกอบแอปหลัก ตัวแปรเดียวกันจะถูกส่งผ่านไปยังทั้ง Navbar และหน้าแรก รูปแบบง่ายๆ ที่ใช้ด้านบนจะอัปเดตตัวแปรสถานะ “ชื่อ” จากคอมโพเนนต์ EditPage

แนวทางทั้งสองที่นำเสนอด้านล่างจะกล่าวถึงรายละเอียดของการนำไปปฏิบัติ:

แนวทางแรก: useState

ใช้สถานะ()

useState เป็นหนึ่งใน React hooks ที่ใช้บ่อยที่สุด ซึ่งช่วยให้คุณสร้างและเปลี่ยนสถานะใน React Functional Component ได้ ส่วนประกอบ useState มีการใช้งานที่ง่ายมากและใช้งานง่าย ในการสร้างสถานะใหม่ คุณต้องเรียก useState ด้วยค่าเริ่มต้นของสถานะของคุณ และ hook useState จะส่งคืนอาร์เรย์ที่มีตัวแปรสองตัว: อันแรกคือสถานะ ตัวแปรที่คุณสามารถใช้เพื่ออ้างอิงสถานะของคุณและตัวแปรตัวที่สองเป็นฟังก์ชันที่คุณใช้ในการเปลี่ยนค่าของสถานะ: ค่อนข้างตรงไปตรงมา

แล้วเราจะเห็นสิ่งนั้นในการดำเนินการได้อย่างไร? ชื่อที่แสดงผลใน Navbar - ด้านขวาบน - เป็นตัวแปรสถานะที่กำหนดไว้ในส่วนประกอบแอปหลัก ตัวแปรเดียวกันจะถูกส่งผ่านไปยังทั้ง Navbar และหน้าแรก รูปแบบง่ายๆ ที่ใช้ด้านบนจะอัปเดตตัวแปรสถานะ “ชื่อ” จากคอมโพเนนต์ EditPage บรรทัดล่างคือ: useState เป็น hook หลักที่ยอมรับสถานะเริ่มต้นเป็นพารามิเตอร์และส่งกลับตัวแปรสองตัวที่มีค่าสองค่า ตัวแปรสถานะที่มีสถานะเริ่มต้น และฟังก์ชัน setter สำหรับตัวแปรสถานะเดียวกัน

มาดูรายละเอียดกันดีกว่าว่ามีการนำไปใช้อย่างไรตั้งแต่แรก

  1. การสร้างตัวแปรสถานะ “ชื่อ”:

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

##อุปกรณ์ประกอบฉาก

อุปกรณ์ประกอบฉากเป็นหนึ่งในหน่วยการสร้างพื้นฐานขององค์ประกอบการตอบสนอง ตามแนวคิดแล้ว หากคุณคิดว่าองค์ประกอบการทำงานของ React เป็นฟังก์ชัน Javascript อุปกรณ์ประกอบฉากก็ไม่มีอะไรมากไปกว่าพารามิเตอร์ของฟังก์ชัน การรวมอุปกรณ์ประกอบฉากและตะขอ useState สามารถให้เฟรมเวิร์กที่แข็งแกร่งแก่คุณได้ สำหรับการจัดการสถานะผ่านแอปพลิเคชัน React แบบง่ายๆ

อุปกรณ์ประกอบฉากปฏิกิริยาจะถูกส่งผ่านเป็นคุณลักษณะไปยังส่วนประกอบที่กำหนดเอง คุณสมบัติที่ส่งผ่านเป็นอุปกรณ์ประกอบฉากสามารถทำลายโครงสร้างจากวัตถุอุปกรณ์ประกอบฉากเมื่อยอมรับว่ามันเป็นอาร์กิวเมนต์ คล้ายกับสิ่งนี้:

###อุปกรณ์ประกอบฉากการส่งผ่าน

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

อุปกรณ์ประกอบฉากสามารถยอมรับและใช้ภายในองค์ประกอบการทำงานที่คล้ายกับอาร์กิวเมนต์ของฟังก์ชันปกติ เป็นเพราะ “ชื่อ” ถูกส่งผ่านเป็นอุปกรณ์ประกอบฉากไปยังคอมโพเนนต์ Home เราจึงสามารถเรนเดอร์มันในคอมโพเนนต์เดียวกันได้ ในตัวอย่างต่อไปนี้ เรากำลังยอมรับ prop ที่ส่งผ่านโดยใช้ไวยากรณ์การทำลายล้างเพื่อแยกคุณสมบัติชื่อออกจากวัตถุอุปกรณ์ประกอบฉาก

รับอุปกรณ์ประกอบฉาก

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

เคล็ดลับมือโปร

เปิดคอนโซลของเบราว์เซอร์ และสังเกตว่าส่วนประกอบทั้งหมดที่ใช้เสา “ชื่อ” จะแสดงผลซ้ำเมื่อสถานะเปลี่ยนแปลงอย่างไร เมื่อจัดการตัวแปรสถานะ React จะจัดเก็บสถานะถัดไป เรนเดอร์ส่วนประกอบของคุณอีกครั้งด้วยค่าใหม่ และอัปเดต UI

Components Re-rendering

ใช้ข้อเสียเปรียบของรัฐ

อุปกรณ์ประกอบฉาก-การขุดเจาะ

การเจาะอุปกรณ์ประกอบฉากเป็นคำที่อ้างถึงลำดับชั้นของส่วนประกอบโดยที่ชุดส่วนประกอบต้องการอุปกรณ์ประกอบฉากบางอย่างที่มาจากส่วนประกอบหลัก วิธีแก้ไขปัญหาทั่วไปที่นักพัฒนาที่ไม่มีประสบการณ์มักจะใช้คือการส่งอุปกรณ์ประกอบฉากเหล่านี้ตลอดห่วงโซ่ส่วนประกอบทั้งหมด ปัญหาของสิ่งนี้ แนวทางคือการเปลี่ยนแปลงอุปกรณ์ประกอบฉากใดๆ เหล่านี้จะกระตุ้นให้เชนส่วนประกอบทั้งหมดเรนเดอร์อีกครั้ง ส่งผลให้แอปพลิเคชันทั้งหมดช้าลงอย่างมีประสิทธิภาพอันเป็นผลมาจากการเรนเดอร์ที่ไม่จำเป็นเหล่านี้ ส่วนประกอบที่อยู่ตรงกลางของเชนที่ไม่ต้องใช้อุปกรณ์ประกอบฉากเหล่านี้ ทำหน้าที่เป็นสื่อกลางในการขนย้ายอุปกรณ์ประกอบฉาก

ตัวอย่าง:

  • ตัวแปรสถานะถูกกำหนดไว้ในองค์ประกอบแอปหลักโดยใช้ตะขอ useState()

  • เสาชื่อถูกส่งไปยังองค์ประกอบ Navbar

  • เสาเดียวกันได้รับการยอมรับใน Navbar และส่งผ่านเป็นเสาอีกครั้งไปยังส่วนประกอบ UserDropdown

  • UserDropdown เป็นองค์ประกอบย่อยสุดท้ายที่ยอมรับเสา

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

ลองนึกภาพดูว่าการรักษาแอป React ที่มีเลเยอร์ส่วนประกอบต่างๆ กันนั้นยากเพียงใดในการส่งผ่านและแสดงผลในสถานะเดียวกัน

ความซับซ้อนที่เพิ่มขึ้นและคุณภาพของรหัส

ด้วยการใช้ useState และอุปกรณ์ประกอบฉากเป็นวิธีเดียวในการจัดการสถานะในแอปพลิเคชันแบบโต้ตอบ โค้ดเบสจะมีความซับซ้อนเพิ่มขึ้นอย่างรวดเร็ว โดยต้องจัดการตัวแปรสถานะนับสิบหรือหลายร้อยตัว ซึ่งสามารถทำซ้ำของตัวแปรอื่น ๆ กระจายไปตามไฟล์ต่างๆ และ ส่วนประกอบต่างๆ อาจค่อนข้างน่ากังวล การเปลี่ยนแปลงใดๆ ในตัวแปรสถานะที่กำหนดจะต้องพิจารณาอย่างรอบคอบถึงการขึ้นต่อกันระหว่างส่วนประกอบต่างๆ เพื่อหลีกเลี่ยงการแสดงผลซ้ำเพิ่มเติมที่อาจเกิดขึ้นในแอปพลิเคชันที่ช้าอยู่แล้ว

แนวทางที่สอง: Context API

Context API เป็นความพยายามของ React ในการแก้ไขข้อเสียของการใช้เฉพาะอุปกรณ์ประกอบฉากและ useState สำหรับการจัดการสถานะ โดยเฉพาะอย่างยิ่ง API บริบทมาเป็นคำตอบสำหรับปัญหาที่กล่าวไว้ก่อนหน้านี้เกี่ยวกับความจำเป็นในการส่งอุปกรณ์ประกอบฉากลงในแผนผังส่วนประกอบทั้งหมด ด้วยการใช้บริบท คุณสามารถกำหนดสถานะของข้อมูลที่คุณเห็นว่าเป็นข้อมูลส่วนกลางและเข้าถึงสถานะของข้อมูลได้จากจุดใดก็ได้ในโครงสร้างส่วนประกอบ: ไม่ต้องเจาะเสาอีกต่อไป

สิ่งสำคัญคือต้องชี้ให้เห็นว่าในตอนแรก Context API ถูกสร้างขึ้นเพื่อแก้ไขปัญหาการแบ่งปันข้อมูลทั่วโลก ข้อมูล เช่น ธีม UI ข้อมูลการตรวจสอบสิทธิ์ซึ่งเป็นกรณีการใช้งานของเรา ภาษา และอื่นๆ) สำหรับข้อมูลประเภทอื่นๆ ที่จำเป็นต้องแชร์ ท่ามกลางองค์ประกอบมากกว่าหนึ่งองค์ประกอบ แต่ไม่จำเป็นต้องเป็นสากลสำหรับแอปพลิเคชันทั้งหมด บริบทอาจไม่ใช่ตัวเลือกที่ดีที่สุด ขึ้นอยู่กับกรณีการใช้งาน คุณอาจพิจารณาเทคนิคอื่น ๆ เช่น องค์ประกอบส่วนประกอบ ซึ่งอยู่นอกขอบเขตของบทความนี้

เปลี่ยนเป็นบริบท

การสร้างบริบทการรับรองความถูกต้อง

createContext นั้นตรงไปตรงมา สร้างตัวแปรบริบทใหม่ โดยรับพารามิเตอร์ทางเลือกตัวเดียว: ค่าเริ่มต้นของตัวแปรบริบท

มาดูกันก่อนว่าสร้างโฟลเดอร์ใหม่ “บริบท” และไฟล์ใหม่ภายในนั้น “Auth.jsx” ในการสร้างบริบทใหม่ เราจำเป็นต้องเรียกใช้ฟังก์ชัน createContext() กำหนดค่าที่ส่งคืนให้กับตัวแปร Auth ใหม่ ซึ่งจะถูกส่งออกต่อไป:

./src/contexts/Auth.jsx

import { createContext } from "react";

export const Auth = createContext();

ระบุบริบทการตรวจสอบสิทธิ์

ตอนนี้เราต้องเปิดเผยตัวแปรบริบท “Auth” ที่เราสร้างไว้ก่อนหน้านี้ เพื่อให้บรรลุเป้าหมายนี้ เราใช้ตัวให้บริการบริบท ตัวให้บริการบริบทเป็นองค์ประกอบที่มีเสา “ค่า” เราสามารถใช้เสานี้เพื่อแบ่งปันค่า - ชื่อ – ข้ามแผนผังส่วนประกอบ ด้วยการรวมแผนผังส่วนประกอบด้วยส่วนประกอบของผู้ให้บริการ เราทำให้ค่านั้นสามารถเข้าถึงได้จากทุกที่ภายในแผนผังส่วนประกอบ โดยไม่จำเป็นต้องส่ง prop นั้นไปยังส่วนประกอบย่อยแต่ละส่วนแยกกัน

ในตัวอย่างการตรวจสอบสิทธิ์ของเรา เราจำเป็นต้องมีตัวแปรเพื่อเก็บอ็อบเจ็กต์ผู้ใช้ และตัวตั้งค่าบางประเภทเพื่อจัดการตัวแปรสถานะนั้น นี่เป็นกรณีการใช้งานที่สมบูรณ์แบบสำหรับ useState เมื่อใช้ Context คุณต้องตรวจสอบให้แน่ใจว่าคุณกำลังกำหนดข้อมูลที่คุณต้องการระบุ และส่งข้อมูลนั้น – ผู้ใช้ในตัวอย่างของเรา – ไปยังแผนผังส่วนประกอบทั้งหมดที่ซ้อนกันอยู่ภายใน ดังนั้นให้กำหนดผู้ใช้ตัวแปรสถานะใหม่ภายในส่วนประกอบของผู้ให้บริการและ ในที่สุดเราก็ส่งทั้งผู้ใช้และ setUser ภายในอาร์เรย์เป็นค่าที่ผู้ให้บริการจะเปิดเผยให้กับแผนผังส่วนประกอบ:

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

อีกสิ่งหนึ่งที่เราทำได้คือย้ายตัวแปรสถานะ “ชื่อ” ของเราจากส่วนประกอบแอปหลักไปยังบริบท Auth และเปิดเผยไปยังส่วนประกอบที่ซ้อนกัน:

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

ตอนนี้สิ่งที่เหลืออยู่คือการซ้อนแอปของเราไว้ในองค์ประกอบ AuthProvider เดียวกับที่เราเพิ่งส่งออก

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

เนื่องจากเรากำลังเรนเดอร์ลูกพร็อพภายใน Auth.Provider องค์ประกอบทั้งหมดที่ซ้อนกันอยู่ภายในคอมโพเนนต์ AuthProvider จึงสามารถใช้ค่าพร็อพค่าที่เราส่งไปยัง Auth.Provider ได้ อาจดูน่าสับสน แต่เมื่อคุณทดลองแล้ว ให้ลองจัดเตรียมและใช้สถานะส่วนกลาง - บริบท - ท้ายที่สุด มันสมเหตุสมผลสำหรับฉันหลังจากทดลองใช้ Context API เท่านั้น

การใช้บริบทการรับรองความถูกต้อง

ขั้นตอนสุดท้ายตรงไปตรงมา เราใช้บริบทฮุค “useContext” เพื่อเข้าถึงค่าที่ผู้ให้บริการบริบทของ “Auth” ให้มา ซึ่งในกรณีของเราคืออาร์เรย์ที่มีผู้ใช้และ setUser ในโค้ดต่อไปนี้ เราสามารถ ใช้ useContext เพื่อใช้บริบทการรับรองความถูกต้องภายใน Navbar สิ่งนี้เป็นไปได้เนื่องจาก Navbar ซ้อนอยู่ในองค์ประกอบของ App และเนื่องจาก AuthProvider ล้อมรอบส่วนประกอบ App จึงสามารถใช้ค่า prop ได้โดยใช้ตะขอ useContext เท่านั้น เครื่องมือที่ยอดเยี่ยมอีกตัวหนึ่งที่ React มอบให้ทันทีเพื่อจัดการข้อมูลใด ๆ ที่สามารถเข้าถึงได้ทั่วโลกและยังถูกจัดการโดยส่วนประกอบของผู้บริโภคอีกด้วย

./src/components/Navbar.jsx

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

 return (...)};

สังเกตว่าเราไม่ยอมรับอุปกรณ์ประกอบฉากใดๆ อีกต่อไปในองค์ประกอบการทำงานของ Navbar() เรากำลังใช้ useContext(Auth) เพื่อใช้บริบท Auth แทน โดยดึงทั้งชื่อและ setName นอกจากนี้ยังหมายความว่าเราไม่จำเป็นต้องส่งต่อเสาไปยัง Navbar อีกต่อไป:

./src/App.jsx

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

กำลังอัปเดตบริบทการตรวจสอบสิทธิ์

นอกจากนี้เรายังสามารถใช้ฟังก์ชัน setName ที่ให้มาเพื่อจัดการตัวแปรสถานะ “ชื่อ”:

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

แนะนำ useReducer() hook

ในตัวอย่างก่อนหน้านี้ เราใช้ context API เพื่อจัดการและแชร์สถานะของเราในแผนผังส่วนประกอบ คุณอาจสังเกตเห็นว่าเรายังคงใช้ useState เป็นฐานของตรรกะของเรา และส่วนใหญ่ก็ถือว่าใช้ได้ เนื่องจากสถานะของเรายังคงเป็นวัตถุธรรมดา ณ จุดนี้ แต่ถ้าเราจะขยายขีดความสามารถของขั้นตอนการตรวจสอบสิทธิ์ของเรา เราจะต้องจัดเก็บมากกว่าแค่อีเมลของผู้ใช้ที่เข้าสู่ระบบอยู่ในปัจจุบัน และนี่คือจุดที่เราจะย้อนกลับไปใช้ข้อจำกัดที่เราพบก่อนหน้านี้ในเรื่องที่เกี่ยวข้องกับ การใช้ useState ที่มีสถานะซับซ้อน โชคดีที่ React สามารถแก้ไขปัญหานี้ได้โดยการจัดหาทางเลือกอื่นให้กับ useState สำหรับการจัดการสถานะที่ซับซ้อน: ป้อน useReducer

useReducer สามารถมองได้ว่าเป็นเวอร์ชันทั่วไปของ useState โดยต้องใช้พารามิเตอร์สองตัว: ฟังก์ชันตัวลดและสถานะเริ่มต้น

ไม่มีอะไรน่าสนใจที่ควรทราบสำหรับสถานะเริ่มต้น ความมหัศจรรย์เกิดขึ้นภายในฟังก์ชันตัวลด: มันจะตรวจสอบประเภทของการกระทำที่เกิดขึ้น และขึ้นอยู่กับการกระทำนั้น ตัวลดจะกำหนดการอัปเดตที่จะนำไปใช้กับสถานะและส่งกลับค่าใหม่ .

เมื่อพิจารณาโค้ดด้านล่าง ฟังก์ชันตัวลดจะมีการดำเนินการที่เป็นไปได้สองประเภท:

  • เข้าสู่ระบบ”: ในกรณีนี้ สถานะผู้ใช้จะได้รับการอัปเดตด้วยข้อมูลรับรองผู้ใช้ใหม่ที่ระบุไว้ภายในเพย์โหลดการดำเนินการ

  • LOGOUT”: ในกรณีนี้สถานะผู้ใช้จะถูกลบออกจากที่จัดเก็บในตัวเครื่องและตั้งค่ากลับเป็นค่าว่าง

สิ่งสำคัญที่ควรทราบคือออบเจ็กต์การดำเนินการมีทั้งฟิลด์ประเภทที่กำหนดตรรกะที่จะใช้และฟิลด์เพย์โหลดเสริมเพื่อให้ข้อมูลที่จำเป็นสำหรับการใช้ตรรกะนั้น

สุดท้าย useReducer hook จะส่งคืนสถานะปัจจุบันและฟังก์ชันการจัดส่งที่เราใช้ในการส่งผ่านการดำเนินการไปยังตัวลด

สำหรับตรรกะที่เหลือ จะเหมือนกับตัวอย่างก่อนหน้านี้:

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

การดำเนินการจัดส่งแทนการใช้ฟังก์ชัน setState setter – เช่น: setName –

ดังที่เราได้กล่าวไปแล้ว เราใช้ฟังก์ชันการจัดส่งเพื่อส่งผ่านการดำเนินการไปยังตัวลด ในโค้ดต่อไปนี้ เราจะเริ่มต้นการดำเนินการ LOGIN และเราจัดเตรียมอีเมลผู้ใช้เป็นเพย์โหลด ตอนนี้สถานะผู้ใช้จะได้รับการอัปเดตและการเปลี่ยนแปลงนี้จะทริกเกอร์ การเรนเดอร์ส่วนประกอบทั้งหมดที่สมัครรับสถานะผู้ใช้อีกครั้ง สิ่งสำคัญคือต้องชี้ให้เห็นว่าการเรนเดอร์ซ้ำจะถูกทริกเกอร์เฉพาะเมื่อมีการเปลี่ยนแปลงจริงในสถานะเกิดขึ้นเท่านั้น และจะไม่มีการเรนเดอร์ซ้ำหากตัวลดส่งคืนสถานะก่อนหน้าเหมือนเดิม

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

เข้าสู่ระบบของผู้ใช้

JWT Local storage

เคล็ดลับมือโปร

สังเกตว่าออบเจ็กต์ผู้ใช้ที่เราได้รับหลังจากการเข้าสู่ระบบสำเร็จถูกจัดเก็บไว้ใน localStorage อย่างไร

ตะขอที่กำหนดเองสำหรับการเข้าสู่ระบบ

ตอนนี้เราเข้าใจ useReducer เป็นอย่างดีแล้ว เราก็สามารถสรุปตรรกะการเข้าสู่ระบบและออกจากระบบของเราเพิ่มเติมลงใน hooks ที่กำหนดเองแยกต่างหากได้ โดยการเรียกไปยัง Hook เข้าสู่ระบบเพียงครั้งเดียว เราสามารถจัดการการเรียก API ไปยังเส้นทางการเข้าสู่ระบบ ดึงข้อมูลผู้ใช้ใหม่ ข้อมูลประจำตัวและจัดเก็บไว้ในที่จัดเก็บในตัวเครื่อง ส่งการเรียก LOGIN เพื่ออัปเดตสถานะผู้ใช้ ทั้งหมดนี้ในขณะที่จัดการกับการจัดการข้อผิดพลาด:

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

หมายเหตุ: สำหรับผู้ใช้ React ขั้นสูงในหมู่ผู้อ่าน คุณอาจสงสัยว่าเหตุใดเราจึงไม่ใช้คุณสมบัติการเริ่มต้นแบบขี้เกียจของ useReducer เพื่อดึงข้อมูลประจำตัวผู้ใช้ useReducer ยอมรับพารามิเตอร์ทางเลือกตัวที่สามที่เรียกว่าฟังก์ชัน init ฟังก์ชันนี้จะใช้ในกรณี เราจำเป็นต้องใช้ตรรกะบางอย่างก่อนที่เราจะได้ค่าเริ่มต้นของสถานะ เหตุผลที่เราไม่ได้เลือกสิ่งนี้ก็เป็นเรื่องง่ายๆ ในการแยกข้อกังวล รหัสในลักษณะนี้เข้าใจง่ายกว่าและผลที่ได้ก็ง่ายกว่าในการดูแลรักษา .

หน้าเข้าสู่ระบบ

ส่วนบนสุดของหน้าเข้าสู่ระบบจะมีลักษณะดังนี้หลังจากใช้ตะขอ useLogin() เพื่อแยกฟังก์ชันการเข้าสู่ระบบ และเรียกใช้ฟังก์ชันการเข้าสู่ระบบด้วยข้อมูลรับรองที่ผู้ใช้ส่งมา:

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

เรายังปิดการใช้งานฟังก์ชันส่งเมื่อผู้ใช้ส่งแบบฟอร์ม:

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

และแสดงข้อผิดพลาดในการรับรองความถูกต้องที่เราได้รับจากแบ็กเอนด์ของเรา:

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

Rendering Errors

การรักษาสถานะผู้ใช้

คุณอาจสงสัยว่าเหตุใดเราจึงต้องจัดเก็บอ็อบเจ็กต์ผู้ใช้ไว้ใน localStorage พูดง่ายๆ ก็คือ เราต้องการให้ผู้ใช้ลงชื่อเข้าใช้ต่อไปตราบใดที่โทเค็นยังไม่หมดอายุ การใช้ localStorage เป็นวิธีที่ดีในการจัดเก็บบิตของ JSON เช่นเดียวกับในตัวอย่างของเรา สังเกตว่าสถานะจะถูกล้างออกไปอย่างไรหากคุณรีเฟรชหน้าหลังจากเข้าสู่ระบบ ซึ่งสามารถแก้ไขได้ง่ายโดยใช้ useEffect hook เพื่อตรวจสอบว่าเรามีอ็อบเจ็กต์ผู้ใช้ที่เก็บไว้ใน localStorage หรือไม่ หากมี เราจะล็อกอินผู้ใช้โดยอัตโนมัติ:

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

hooks ที่กำหนดเองสำหรับการออกจากระบบ

สิ่งเดียวกันนี้ใช้ได้กับ Logout hook ที่นี่เรากำลังส่งการดำเนินการ LOGOUT เพื่อลบข้อมูลรับรองผู้ใช้ปัจจุบันออกจากทั้งสถานะและที่จัดเก็บในตัวเครื่อง:

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

ผู้ใช้ออกจากระบบ

หากต้องการออกจากระบบผู้ใช้ ให้เพิ่มเหตุการณ์คลิกไปที่ปุ่มออกจากระบบที่พบใน UserDropdown.jsx และจัดการตามนั้น:

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

การปกป้องเส้นทางปฏิกิริยา

ขั้นตอนสุดท้ายในการใช้งานแอปพลิเคชันของเราคือการใช้ประโยชน์จากสถานะผู้ใช้ทั่วโลกเพื่อควบคุมการนำทางของผู้ใช้ ซึ่งเป็นการเตือนอย่างรวดเร็วเกี่ยวกับพฤติกรรมที่เราควรทำ: เริ่มแรกผู้ใช้จะได้รับการต้อนรับจากหน้าเข้าสู่ระบบ จากนั้นผู้ใช้จะสามารถเข้าถึงหน้าแรกได้เท่านั้น หลังจากเข้าสู่ระบบสำเร็จ ในทำนองเดียวกันผู้ใช้จะถูกเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบเมื่อออกจากระบบ

เราบรรลุเป้าหมายนี้ด้วยความช่วยเหลือของไลบรารี react-router-dom โดยการกำหนด 2 เส้นทาง: ”/” และ “/login” เราควบคุมว่าองค์ประกอบใดที่จะแสดงผลในแต่ละเส้นทางโดยใช้สถานะการรับรองความถูกต้องทั่วโลก การรับรองความถูกต้องประเมินเป็น null แสดงถึงผู้ใช้ที่ไม่ได้รับการรับรองความถูกต้องและในทางกลับกัน:

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

แผนภาพสรุป

Diagram

สรุปแล้ว

ในบทความนี้ เราได้พยายามจัดการกับกรณีการใช้งานที่เรียบง่ายแต่พบได้บ่อยมากในการดำเนินการการจัดการสถานะสำหรับเวิร์กโฟลว์การตรวจสอบสิทธิ์ โดยพิจารณาวิธีการต่างๆ เหตุผลเบื้องหลังแต่ละวิธี และข้อดีข้อเสีย การจัดการสถานะในเฟรมเวิร์กฝั่งไคลเอ็นต์และโดยเฉพาะอย่างยิ่งใน React เป็นหนึ่งในหัวข้อที่ได้รับการพูดถึงมากที่สุดในชุมชนส่วนหน้า เพียงเพราะมันสามารถสร้างหรือทำลายประสิทธิภาพและความสามารถในการปรับขนาดของแอปพลิเคชันของคุณได้ เทคนิค รูปแบบ ไลบรารี และเครื่องมือต่างๆ จำนวนมากที่พยายามแก้ไขปัญหาการจัดการของรัฐนี้มีอย่างล้นหลาม เป้าหมายของเราคือการทำให้คุณมีความเข้าใจอย่างถ่องแท้เกี่ยวกับแนวทางปฏิบัติ เพื่อให้คุณสามารถนำไปใช้ในแอปพลิเคชันของคุณเอง สำหรับขั้นสูงยิ่งขึ้น เทคนิคและรูปแบบของการจัดการสถานะในแอปพลิเคชันที่ซับซ้อนมากขึ้น โปรดดูบทความถัดไปที่เราพูดถึง Redux สำหรับการจัดการสถานะที่ปรับขนาดได้ใน React

เร็วๆ นี้

ContextAPI กับชุดเครื่องมือ Redux

Redux มีการนำไปใช้ในชุมชนเป็นเวลาหลายปีก่อนที่จะมีชุดเครื่องมือ Redux ในความเป็นจริง RTK ได้รับการแนะนำเป็นเทมเพลตเริ่มต้นเพื่อบูตการจัดการสถานะ redux ในแอปพลิเคชันใหม่ (ชื่อเริ่มต้นคือ “redux-starter-kit” ในเดือนตุลาคม 2019) แม้ว่า ในปัจจุบัน มีความเห็นพ้องต้องกันโดยทั่วไประหว่างผู้ดูแล Redux และชุมชนว่าชุดเครื่องมือ Redux เป็นวิธีที่ถูกต้องในการทำงานกับ redux RTX สรุปตรรกะ Redux จำนวนมากซึ่งทำให้ใช้งานง่ายขึ้น โดยมีรายละเอียดน้อยกว่ามาก และสนับสนุนให้นักพัฒนา ปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุด ข้อแตกต่างหลักประการหนึ่งระหว่างทั้งสองคือ Redux ถูกสร้างขึ้นมาให้ไม่มีความคิดเห็น โดยจัดให้มี API ขั้นต่ำ และคาดหวังให้นักพัฒนาทำงานหนักส่วนใหญ่โดยการเขียนไลบรารีของตนเองสำหรับงานทั่วไปและจัดการกับโครงสร้างโค้ด สิ่งนี้ส่งผลให้เวลาในการพัฒนาช้าลงและโค้ดที่ยุ่งเหยิง ชุดเครื่องมือ Redux ถูกเพิ่มเป็นชั้นพิเศษของสิ่งที่เป็นนามธรรมเพื่อป้องกันไม่ให้นักพัฒนาตกอยู่ในข้อผิดพลาดทั่วไป โปรดดูข้อมูลเชิงลึกเพิ่มเติมและเหตุผลจากผู้ดูแลในเอกสารอย่างเป็นทางการ โปรดดูที่เอกสารประกอบอย่างเป็นทางการ /redux.js.org/introduction/why-rtk-is-redux-today)