Kodokon kodokon.com

useReducer สำหรับ state ที่ซับซ้อน

จัดโครงสร้าง state ที่ซับซ้อนด้วย reducer แบบ pure ที่รวมศูนย์การเปลี่ยนแปลงและตัดคอมบิเนชันที่เป็นไปไม่ได้ออกไป

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

ทันทีที่ component หนึ่งต้องประสาน state หลายค่าที่เปลี่ยนไปพร้อมกัน - สถานะของ request, ข้อมูลบางอย่าง, ข้อผิดพลาด - การเพิ่มจำนวนการเรียก useState จะทำให้ logic ของการเปลี่ยนสถานะกระจัดกระจายไปตาม event handler ทุกตัว คุณรู้อาการดี: มี setter สามตัวที่ต้องเรียกให้ถูกลำดับ และไม่ช้าก็เร็วก็จะเกิด state ที่ไม่สอดคล้องกัน เช่นสถานะ success ที่อยู่ข้างๆ error ที่ไม่ใช่ null useReducer พลิกความรับผิดชอบนี้: state กลายเป็น object เดียว และทุกการเปลี่ยนแปลงจะผ่าน action ที่ตีความโดยฟังก์ชันกลางเพียงตัวเดียวคือ reducer

JAVASCRIPT
function requestReducer(state, action) {
  switch (action.type) {
    case "start":
      return { status: "loading", data: null, error: null };
    case "resolve":
      return {
        status: "success",
        data: action.data,
        error: null,
      };
    case "reject":
      return {
        status: "error",
        data: null,
        error: action.error,
      };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}
การเปลี่ยนสถานะแต่ละครั้งสร้าง state ที่สมบูรณ์และสอดคล้องกันขึ้นใหม่

reducer เป็นฟังก์ชันแบบ pure: มันรับ state ปัจจุบันกับ action แล้วคืน state ถัดไป โดยไม่มี side effect ประโยชน์เห็นได้ทันที: การเปลี่ยนสถานะ ชัดเจนและครบถ้วน, state ที่เป็นไปไม่ได้หายไปตั้งแต่ต้นทาง (case แต่ละตัวสร้าง object ที่สอดคล้องกันขึ้นใหม่) และ logic อ่านได้ที่จุดเดียวแทนที่จะกระจัดกระจายไปทั่ว สัญญาณเตือนที่มีประโยชน์: ถ้าคุณพบว่าตัวเองก็อปวางลำดับการเรียก setState ชุดเดิมในสองที่ คุณน่าจะกำลังขาด reducer อยู่

JSX
function UserProfile() {
  const [state, dispatch] = useReducer(requestReducer, {
    status: "idle",
    data: null,
    error: null,
  });

  async function load(id) {
    dispatch({ type: "start" });
    try {
      const res = await fetch("/api/users/" + id);
      const data = await res.json();
      dispatch({ type: "resolve", data });
    } catch (error) {
      dispatch({ type: "reject", error });
    }
  }

  return <button onClick={() => load(1)}>Load</button>;
}
component แสดงเจตนา ส่วน reducer เป็นผู้ตัดสิน state

การเลือกระหว่าง useState กับ useReducer ไม่ใช่เรื่องของรสนิยม ใช้ useState ต่อไปสำหรับค่าที่เรียบง่ายและเป็นอิสระ; เปลี่ยนไปใช้ useReducer เมื่อ state ถัดไปขึ้นอยู่กับค่าก่อนหน้าหรือเมื่อหลาย field เปลี่ยนไปพร้อมกัน ข้อดีเพิ่มเติมอีกสองอย่าง: อาร์กิวเมนต์ตัวที่สามให้ การ initialize แบบ lazy (useReducer(reducer, arg, init) จะเรียก init เฉพาะตอน render ครั้งแรก) และ React รับประกันว่า dispatch จะคง identity เดิมจาก render หนึ่งไปยังอีก render หนึ่ง - คุณส่งมันลงไปให้ลูกได้โดยไม่ต้องใช้ useCallback

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. สัญญาณใดที่ควรชี้ให้คุณหันไปใช้ useReducer แทนการเรียก useState หลายตัว?
    • component ยาวเกินร้อยบรรทัด
    • ค่า state หลายค่าเปลี่ยนไปพร้อมกันและ state ใหม่ขึ้นอยู่กับค่าก่อนหน้า
    • component รับ props มากกว่าสามตัว
    • state มี array อยู่ข้างใน
  2. reducer คืน object ของ state ที่มันได้รับมา โดย mutate ในที่เดิม React จะทำอย่างไร?
    • มันเปรียบเทียบ reference เก่ากับใหม่ พบว่าเท่ากัน แล้วข้ามการ re-render
    • มันตรวจจับการ mutate ในเชิงลึกและ re-render component
    • มันโยน error เพราะ state ถูก freeze ในโหมด development
  3. ทำไมคุณจึงส่ง dispatch ให้ลูกที่ถูก memoize ได้โดยไม่ต้องระวังเป็นพิเศษ?
    • เพราะ React.memo ไม่สนใจ props ที่เป็นชนิดฟังก์ชัน
    • เพราะ dispatch ถูกสร้างใหม่แต่เท่ากันในเชิงโครงสร้างในทุก render
    • เพราะ React รับประกันว่า dispatch จะคง identity เดิมตลอดอายุการใช้งานของ component