Kodokon kodokon.com

Context: การแบ่งปันโดยไม่ต้อง prop drilling

ย้ายข้อมูลไปมาโดยไม่ต้อง prop drilling ด้วย context พร้อมทั้งควบคุมการ re-render และข้อจำกัดของมันให้อยู่ในมือ

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

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

Prop drilling หมายถึง props เหล่านั้นที่ผ่าน component คั่นกลางสามหรือสี่ตัวโดยไม่ได้ถูกใช้ที่นั่นเลย เพียงเพื่อไปให้ถึงใบของต้นไม้ ทุกชั้นที่มันผ่านก็จะผูกติดกับข้อมูลที่มันไม่ได้สนใจ Context แก้ปัญหาการขนส่งนี้: createContext สร้างช่องทาง, provider เผยแพร่ค่า และ useContext อ่านมันจาก descendant ตัวใดก็ได้ จำความละเอียดอ่อนนี้ไว้: context ขนส่ง state มันไม่ได้ จัดการ state - ค่ายังคงอยู่ใน component หนึ่ง ในที่นี้คือ provider

JSX
import { createContext, useMemo, useState } from "react";

const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("dark");
  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
ค่าที่เผยแพร่ถูก memoize เพื่อให้ reference ของมันคงที่

เมื่อค่าของ provider เปลี่ยน consumer ทั้งหมดจะ re-render แม้แต่ตัวที่ซ้อนลึก - และ React.memo ก็ไม่ได้ปกป้องพวกมัน เพราะมันเปรียบเทียบ props ไม่ใช่ context กับดักคลาสสิกยิ่งทำให้แย่ลง: การเขียน value={{ theme, setTheme }} สร้าง object ใหม่ในทุก render ของ provider จึงเกิด reference ใหม่ จึงเกิด re-render ต่อเนื่องเป็นลูกโซ่แม้ theme ยังไม่ได้ขยับ นั่นคือหน้าที่ของ useMemo ข้างบน ต่อยอดการตั้งค่าด้วย access hook ที่ล้มเหลวอย่างส่งเสียงดังเมื่ออยู่นอก provider

JSX
import { useContext } from "react";

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error(
      "useTheme must be inside <ThemeProvider>"
    );
  }
  return context;
}
error ที่ชัดเจนดีกว่า null ที่เงียบงัน

ก่อนสร้าง context ให้ใช้ตัวเลือก composition จนหมดก่อน ถ้าชั้นคั่นกลางไม่ได้ทำอะไรนอกจากส่งต่อ prop ให้พลิกโครงสร้าง: สร้าง element ไว้ที่ด้านบนของต้นไม้ตรงที่ข้อมูลอยู่ แล้วส่งมันลงไปผ่าน children หรือ prop เฉพาะทาง ชั้นคั่นกลางจะกลายเป็น generic และข้อมูลก็ไม่ผ่าน component ใดที่ไม่ได้ใช้มันอีกต่อไป

JSX
function App({ currentUser }) {
  return (
    <Layout
      header={<TopBar user={currentUser} />}
      content={<Dashboard />}
    />
  );
}
Layout ไม่รู้อะไรเลยเกี่ยวกับ currentUser: มันแค่เสียบ element ที่สร้างเสร็จแล้วเข้าไป

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

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

  1. consumer ของ context ถูกห่อด้วย React.memo และค่าของ context เปลี่ยน จะเกิดอะไรขึ้น?
    • มันไม่ re-render: React.memo บล็อกการอัปเดตทั้งหมด
    • มัน re-render: React.memo เปรียบเทียบ props ไม่ใช่ค่าของ context
    • มัน re-render เฉพาะเมื่อ props ของมันเปลี่ยนด้วยเท่านั้น
  2. ทำไมต้อง memoize object ที่ส่งเข้าไปใน value ของ provider?
    • เพื่อเร่งความเร็วในการสร้าง object ในแต่ละ render
    • เพราะ provider ต้องการค่าที่ immutable
    • เพื่อให้ reference เปลี่ยนเฉพาะเมื่อข้อมูลเปลี่ยน หลีกเลี่ยงการ re-render ที่ไม่จำเป็นของ consumer
  3. ข้อจำกัดหลักของ context สำหรับ state ที่อัปเดตบ่อยมากคืออะไร?
    • ไม่มี selector: ทุกการอัปเดต re-render consumer ทั้งหมด แม้แต่ตัวที่ใช้เพียงส่วนหนึ่งของค่า
    • Context เป็นแบบ asynchronous และหน่วงการอัปเดต
    • Context เก็บได้เฉพาะค่า primitive เท่านั้น