Kodokon kodokon.com

Experten-Patterns: Compound Components, Inversion of Control

Entwirf flexible Komponenten-APIs mit Compound Components, dem State Reducer und der richtigen Wahl zwischen Render Props und Hooks.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Eine <Tabs items={...} labels={...} icons={...} />-Komponente versinkt am Ende immer unter Konfigurations-Props. Das Compound-Components-Pattern dreht den Ansatz um: Mehrere Komponenten arbeiten zusammen und teilen sich impliziten State über einen Kontext, während der Nutzer die Kontrolle über die JSX-Komposition behält (Reihenfolge, Verschachtelung, dazwischengeschobene Elemente). Es ist das Modell von <select> und <option> in HTML und das, was Bibliotheken wie Radix oder Headless UI verwenden.

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>
  );
}
Die Eltern-Komponente stellt geteilten State über einen memoisierten Kontext bereit.

Jede Unterkomponente liest den Kontext und braucht überhaupt keine Verdrahtungs-Props: Der Nutzer komponiert frei, und das Pattern garantiert die Konsistenz. Beachte den in useMemo verpackten Kontextwert: Ohne ihn würde jeder Render der Eltern-Komponente ein neues Objekt erzeugen und einen Render aller Consumer erzwingen.

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>
Keine Verdrahtungs-Props: Der Kontext stellt die Verbindung her.

Zweites Pattern: Inversion of Control, deren verfeinertste Form der State Reducer ist, den Kent C. Dodds in Downshift bekannt gemacht hat. Statt für jeden neuen Bedarf eine Boolean-Prop hinzuzufügen, legt der Hook seine State-Übergänge offen: Der Aufrufer stellt einen Reducer bereit, der jede Action erhält und sie durchlassen, verändern oder abbrechen kann. Die Bibliothek behält ihr Standardverhalten; die feingranulare Kontrolle liegt beim Consumer.

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 };
}
Der Aufrufer kann jeden State-Übergang abfangen.

Schließlich die ewige Debatte Render Props gegen Hooks. Hooks haben sich fürs Teilen von Logik durchgesetzt: keine falsche Hierarchie im Baum, keine Pyramide aus Wrappern, natürliche Komposition mehrerer Quellen. Doch die Render Prop behält ein berechtigtes Gebiet: wenn die Variation das Rendering selbst betrifft. Eine virtualisierte Liste, die dich fragt, wie sie jede Zeile zeichnen soll, eine Headless-Komponente, die ihr gesamtes JSX delegiert: Dort bleibt eine als Prop übergebene Render-Funktion das direkteste Werkzeug, denn ein Hook kann nicht entscheiden, wo JSX im Baum des Aufrufers eingefügt wird.

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();
Eine Render Prop, um das Rendering zu delegieren, ein Hook für die Logik.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Welcher Mechanismus lässt Compound Components ihren State ohne explizite Props teilen?
    • Eine gemeinsame Modulvariable
    • Ein von der Eltern-Komponente bereitgestellter React-Kontext
    • Den State in jedem Kind duplizieren
    • Über cloneElement weitergereichte Refs
  2. Was genau kehrt das State-Reducer-Pattern um?
    • Die Richtung des Datenflusses zwischen Eltern und Kind
    • Die Kontrolle über die State-Übergänge, die an den Aufrufer übergeben wird
    • Die Ausführungsreihenfolge der Hooks
    • Die Verantwortung fürs Rendern des Fallbacks
  3. In welchem Fall bleibt eine Render Prop einem Hook vorzuziehen?
    • Wenn du Logik zwischen Komponenten teilen willst
    • Wenn der Aufrufer das JSX bereitstellen muss, das in den Baum der Komponente eingefügt wird
    • Wenn du eine teure Berechnung memoisieren musst
    • Wenn die Komponente keinen State hat