コンパウンドコンポーネント、state reducer、そして render props とフックの適切な選択によって、柔軟なコンポーネント API を設計します。
このレッスンを Kodokon で開く<Tabs items={...} labels={...} icons={...} /> というコンポーネントは、いつも設定用の props に埋もれてしまいます。コンパウンドコンポーネント パターンはアプローチを逆転させます。複数のコンポーネントが協調し、コンテキストを通じて 暗黙的な state を共有する一方で、ユーザーは JSX の構成(順序、ネスト、要素の挟み込み)の主導権を保ちます。これは HTML の <select> と <option> のモデルであり、Radix や Headless UI のようなライブラリで使われているものです。
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>
);
}各サブコンポーネントはコンテキストを読み取り、配線用の props をまったく必要としません。ユーザーは自由に構成でき、パターンが一貫性を保証します。コンテキストの値が useMemo でラップされていることに注目してください。これがないと、親のレンダーのたびに新しいオブジェクトが生成され、すべての コンシューマーのレンダーが強制されてしまいます。
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>2 つ目のパターン: 制御の反転。その最も洗練された形が、Kent C. Dodds が Downshift で広めた state reducer です。新しいニーズが出るたびに boolean の prop を追加するのではなく、フックは自身の state 遷移 を公開します。呼び出し側が reducer を提供し、それが各アクションを受け取って、通過させるか、変更するか、キャンセルするかを決められます。ライブラリはデフォルトの挙動を保ち、きめ細かな制御はコンシューマーに委ねられます。
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 対フック の議論です。ロジックの共有 においてはフックが勝利しました。ツリー内の偽の階層もなく、ラッパーのピラミッドもなく、複数のソースを自然に組み合わせられます。しかし render prop には正当な領域が残っています。変化が レンダリングそのもの に関わる場合です。各行の描き方を尋ねてくる仮想化リスト、すべての JSX を委譲するヘッドレスコンポーネント。そこでは prop として渡される render 関数が最も直接的なツールであり続けます。フックは呼び出し側のツリーの どこに 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();