Kodokon kodokon.com

แพตเทิร์นระดับผู้เชี่ยวชาญ: compound components, การกลับด้านการควบคุม (inversion of control)

ออกแบบ API ของคอมโพเนนต์ที่ยืดหยุ่นด้วย compound components, state reducer, และการเลือกที่ถูกต้องระหว่าง render props กับ hooks

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

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

คอมโพเนนต์ <Tabs items={...} labels={...} icons={...} /> มักจะจบลงด้วยการถูกฝังอยู่ใต้พร็อพการกำหนดค่ามากมายเสมอ แพตเทิร์น compound components พลิกวิธีคิด: คอมโพเนนต์หลายตัวร่วมมือกันและแบ่งปัน สเตตโดยปริยาย (implicit state) ผ่านคอนเท็กซ์ (context) ในขณะที่ผู้ใช้ยังคงควบคุม การประกอบ JSX (ลำดับ การซ้อน การสอดแทรกเอลิเมนต์) ได้ นี่คือโมเดลของ <select> และ <option> ใน HTML และเป็นโมเดลที่ไลบรารีอย่าง Radix หรือ Headless UI ใช้

JSX
const TabsContext = createContext(null);

function Tabs({ defaultTab, children }) {
  const [active, setActive] = useState(defaultTab);
  const value = useMemo(
    () => ({ active, setActive }),
    [active]
  );
  return (
    <TabsContext.Provider value={value}>
      {children}
    </TabsContext.Provider>
  );
}
พาเรนต์เปิดเผยสเตตที่แบ่งปันกันผ่านคอนเท็กซ์ที่ถูก memoize

แต่ละซับคอมโพเนนต์อ่านคอนเท็กซ์และไม่จำเป็นต้องมี พร็อพเชื่อมต่อ (wiring props) ใด ๆ เลย: ผู้ใช้ประกอบได้อย่างอิสระ และแพตเทิร์นรับประกันความสอดคล้อง สังเกตค่าคอนเท็กซ์ที่ห่อไว้ใน useMemo: หากไม่มีมัน ทุกการเรนเดอร์ของพาเรนต์จะสร้างอ็อบเจกต์ใหม่และบังคับให้ผู้บริโภค (consumer) ทั้งหมด เรนเดอร์ใหม่

JSX
function Tab({ id, children }) {
  const ctx = useContext(TabsContext);
  return (
    <button
      aria-selected={ctx.active === id}
      onClick={() => ctx.setActive(id)}
    >
      {children}
    </button>
  );
}

// <Tabs defaultTab="a">
//   <Tab id="a">Profile</Tab>
//   <Tab id="b">Settings</Tab>
// </Tabs>
ไม่มีพร็อพเชื่อมต่อ: คอนเท็กซ์เป็นตัวเชื่อมโยงเอง

แพตเทิร์นที่สอง: การกลับด้านการควบคุม (inversion of control) ซึ่งรูปแบบที่ประณีตที่สุดคือ state reducer ที่ Kent C. Dodds ทำให้เป็นที่นิยมใน Downshift แทนที่จะเพิ่มพร็อพแบบบูลีนสำหรับทุกความต้องการใหม่ ฮุกจะเปิดเผย การเปลี่ยนสเตต (state transitions) ของมัน: ผู้เรียก (caller) จัดหา reducer ที่รับแต่ละแอ็กชัน (action) และสามารถปล่อยผ่าน แก้ไข หรือยกเลิกมันได้ ไลบรารียังคงพฤติกรรมเริ่มต้นไว้; การควบคุมแบบละเอียดเป็นของผู้บริโภค

JSX
function defaultReducer(state, action) {
  if (action.type === 'toggle') {
    return { on: !state.on };
  }
  return state;
}

function useToggle({ reducer = defaultReducer } = {}) {
  const [state, dispatch] = useReducer(reducer, {
    on: false,
  });
  const toggle = () => dispatch({ type: 'toggle' });
  return { on: state.on, toggle };
}
ผู้เรียกสามารถดักจับทุกการเปลี่ยนสเตตได้

สุดท้าย คือการถกเถียงชั่วนิรันดร์ระหว่าง render props กับ hooks ฮุกชนะไปแล้วในเรื่อง การแบ่งปันตรรกะ (sharing logic): ไม่มีลำดับชั้นปลอมในต้นไม้ ไม่มีพีระมิดของตัวห่อ (wrapper) การประกอบแหล่งที่มาหลายแหล่งเป็นไปตามธรรมชาติ แต่ render prop ยังคงมีดินแดนที่ชอบธรรมของมัน: เมื่อความแตกต่างเกี่ยวข้องกับ ตัวการเรนเดอร์เอง ลิสต์แบบเวอร์ชวลไลซ์ (virtualized list) ที่ถามคุณว่าจะวาดแต่ละแถวอย่างไร คอมโพเนนต์แบบ headless ที่มอบหมาย JSX ทั้งหมดของมัน: ในกรณีเหล่านี้ ฟังก์ชันเรนเดอร์ที่ส่งเป็นพร็อพยังคงเป็นเครื่องมือที่ตรงที่สุด เพราะฮุกไม่สามารถตัดสินใจได้ว่าจะแทรก JSX ที่ไหน ในต้นไม้ของผู้เรียก

JSX
function MouseTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const onMove = (e) =>
    setPos({ x: e.clientX, y: e.clientY });
  return (
    <div onMouseMove={onMove}>{render(pos)}</div>
  );
}

// For the logic alone, a hook is enough:
// const pos = useMousePosition();
render prop เพื่อมอบหมายการเรนเดอร์, ฮุกสำหรับตรรกะ

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

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

  1. กลไกใดที่ทำให้ compound components แบ่งปันสเตตของพวกมันได้โดยไม่ต้องใช้พร็อพอย่างชัดแจ้ง?
    • ตัวแปรระดับโมดูลที่แบ่งปันกัน
    • คอนเท็กซ์ React ที่จัดหาโดยคอมโพเนนต์พาเรนต์
    • การทำสำเนาสเตตในแต่ละลูก
    • ref ที่ส่งลงไปผ่าน cloneElement
  2. แพตเทิร์น state reducer กลับด้านอะไรกันแน่?
    • ทิศทางของการไหลของข้อมูลระหว่างพาเรนต์และลูก
    • การควบคุมการเปลี่ยนสเตต ซึ่งถูกมอบให้แก่ผู้เรียก
    • ลำดับการทำงานของฮุก
    • ความรับผิดชอบในการเรนเดอร์ fallback
  3. ในกรณีใดที่ render prop ยังคงเป็นที่นิยมมากกว่าฮุก?
    • เมื่อคุณต้องการแบ่งปันตรรกะระหว่างคอมโพเนนต์
    • เมื่อผู้เรียกต้องจัดหา JSX ที่จะแทรกเข้าไปในต้นไม้ของคอมโพเนนต์
    • เมื่อคุณต้องการ memoize การคำนวณที่มีต้นทุนสูง
    • เมื่อคอมโพเนนต์ไม่มีสเตต