Entwirf flexible Komponenten-APIs mit Compound Components, dem State Reducer und der richtigen Wahl zwischen Render Props und Hooks.
Diese Lektion in Kodokon öffnenEine <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.
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>
);
}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.
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>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.
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 };
}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.
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();