はじめに
久しくReactを触っていなくて設計知識も抜けているので、デザインパターンを勉強しようと思い、Compound Components Patternについてまとめてみました。
Compound Components Pattern とは
Compound Components Pattern(複合コンポーネントパターン) とは、親コンポーネントが共有する状態や API を提供し、子コンポーネントは親からの入力(Context 経由など)で振る舞うデザインパターンです。
子は親の内部で自由に配置でき、親は子の数や構成に依存しない API を提供します。Context を使うと深いネストでも状態共有が簡単になります。
メリット
- API がまとまり、消費側の使い勝手が良くなる
- 子コンポーネントを自由に組み替えられる柔軟性
デメリット
- Context の値設計を間違えると不必要な再レンダリングが起きる
- Context は Provider の
valueが変わると、購読している子がまとめて再レンダリングされやすい - 過剰に凝った設計は可読性・再利用性を下げる
いつ使うべきか
- 複数の子コンポーネントが同じ状態に依存する
- 子の並びや数が柔軟で、親が API を提供したい
避けるべきか
- 単純な一要素だけの UI(オーバーヘッドになる)
実装例
App.jsx
import React from 'react'
import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'
const App = () => (
<Tabs defaultIndex={1}>
<TabList>
<Tab index={0}>Tab A</Tab>
<Tab index={1}>Tab B</Tab>
<Tab index={2}>Tab C</Tab>
</TabList>
<TabPanels>
<TabPanel index={0}>Content A</TabPanel>
<TabPanel index={1}>Content B</TabPanel>
<TabPanel index={2}>Content C</TabPanel>
</TabPanels>
</Tabs>
)
export default App
Tabs.jsx
import React, { createContext, useContext, useMemo, useState, useCallback } from 'react'
const TabsContext = createContext(null)
export const Tabs = ({ children, defaultIndex = 0, index, onChange }) => {
const isControlled = typeof index === 'number'
const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultIndex)
const activeIndex = isControlled ? index : uncontrolledIndex
const onSelect = useCallback(
(i) => {
if (!isControlled) setUncontrolledIndex(i)
if (onChange) onChange(i)
},
[isControlled, onChange]
)
const value = useMemo(() => ({ activeIndex, onSelect }), [activeIndex, onSelect])
return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>
}
export const useTabs = () => {
const ctx = useContext(TabsContext)
if (!ctx) throw new Error('Tabs components must be used within Tabs')
return ctx
}
export const TabList = ({ children }) => <div role="tablist">{children}</div>
export const Tab = ({ children, index }) => {
const { activeIndex, onSelect } = useTabs()
const selected = activeIndex === index
return (
<button
type="button"
role="tab"
aria-selected={selected}
onClick={() => onSelect(index)}
style={{ fontWeight: selected ? 'bold' : 'normal' }}
>
{children}
</button>
)
}
export const TabPanels = ({ children }) => <div>{children}</div>
export const TabPanel = ({ children, index }) => {
const { activeIndex } = useTabs()
return (
<div role="tabpanel" hidden={activeIndex !== index}>
{children}
</div>
)
}
まとめ
- Compound Components Patternとは、親コンポーネントが共有する状態や API を提供し、子コンポーネントは親からの入力(Context 経由など)で振る舞うデザインパターン