react-patterns について
再利用可能なインターフェースを設計する開発者向けの、Reactコンポーネント設計パターンを解説するスキルです。状態やロジックの共有、柔軟な描画、アクセシブルなAPIを扱います。
主な機能
- 01複合コンポーネント
- 02レンダープロップ
- 03高階コンポーネント
- 04カスタムフックとプロバイダー
- 05制御・非制御入力
- 06プロップゲッターと状態リデューサー
活用シーン
- 01再利用可能なコンポーネントAPIの構築
- 02関連コンポーネント間の状態共有
- 03コンポーネント間での状態ロジック再利用
- 04柔軟なフォーム入力の設計
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill react-patternsインストール後、Claude Code で/react-patternsを入力して読み込まれたか確認します。
同梱ファイルreferences/hoc-hooks-provider-controlled-patterns.md
詳細
クイックリファレンス
| パターン | ユースケース | 例 |
|---|---|---|
| 複合コンポーネント | 状態を共有する関連コンポーネント | <Tabs><Tab /><Panel /></Tabs> |
| Render Props | ロジックを共有した動的なレンダリング | <Mouse>{({x,y}) => ...}</Mouse> |
| HOC | 横断的な関心事 | withAuth(Component) |
| カスタムフック | 状態を持つロジックの再利用 | useToggle(), useDebounce() |
| Provider パターン | グローバル/共有状態 | <CartProvider>...</CartProvider> |
| 制御/非制御 | フォーム入力の柔軟性 | value と defaultValue |
| Prop Getter | アクセシブルなコンポーネント API | getButtonProps() |
| State Reducer | カスタマイズ可能な状態ロジック | useReducer(customReducer) |
このスキルを使う場面
React コンポーネントのアーキテクチャに使用します。
- 柔軟で再利用可能なコンポーネント API の構築
- 複合コンポーネントパターンの実装
- 動的なコンテンツ向けの Render Props の作成
- ロジックを再利用するためのカスタムフックの開発
- 制御入力と非制御入力の管理
- アクセシブルなコンポーネントインターフェースの設計
状態管理については、react-state-managementを参照してください。
React コンポーネントパターン
複合コンポーネント
基本的な複合コンポーネント
import { createContext, useContext, useState, ReactNode } from 'react';
// Context for sharing state
interface TabsContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) {
throw new Error('Tabs components must be used within a Tabs provider');
}
return context;
}
// Parent component
interface TabsProps {
defaultTab: string;
children: ReactNode;
}
function Tabs({ defaultTab, children }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
// Tab List
function TabList({ children }: { children: ReactNode }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
// Tab Button
interface TabProps {
value: string;
children: ReactNode;
}
function Tab({ value, children }: TabProps) {
const { activeTab, setActiveTab } = useTabsContext();
return (
<button
role="tab"
aria-selected={activeTab === value}
onClick={() => setActiveTab(value)}
className={activeTab === value ? 'active' : ''}
>
{children}
</button>
);
}
// Tab Panel
interface TabPanelProps {
value: string;
children: ReactNode;
}
function TabPanel({ value, children }: TabPanelProps) {
const { activeTab } = useTabsContext();
if (activeTab !== value) return null;
return (
<div role="tabpanel" className="tab-panel">
{children}
</div>
);
}
// Attach sub-components
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
export { Tabs };
// Usage
function App() {
return (
<Tabs defaultTab="overview">
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="features">Features</Tabs.Tab>
<Tabs.Tab value="pricing">Pricing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="features">Features content</Tabs.Panel>
<Tabs.Panel value="pricing">Pricing content</Tabs.Panel>
</Tabs>
);
}
Slot パターンを使った柔軟な複合コンポーネント
import { createContext, useContext, useState, ReactNode, isValidElement, Children, cloneElement } from 'react';
interface AccordionContextType {
openItems: Set<string>;
toggleItem: (id: string) => void;
allowMultiple: boolean;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
interface AccordionProps {
children: ReactNode;
allowMultiple?: boolean;
defaultOpen?: string[];
}
function Accordion({ children, allowMultiple = false, defaultOpen = [] }: AccordionProps) {
const [openItems, setOpenItems] = useState<Set<string>>(new Set(defaultOpen));
const toggleItem = (id: string) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!allowMultiple) next.clear();
next.add(id);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggleItem, allowMultiple }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
interface AccordionItemProps {
id: string;
children: ReactNode;
}
function AccordionItem({ id, children }: AccordionItemProps) {
const context = useContext(AccordionContext);
if (!context) throw new Error('AccordionItem must be within Accordion');
const isOpen = context.openItems.has(id);
return (
<div className={`accordion-item ${isOpen ? 'open' : ''}`} data-state={isOpen ? 'open' : 'closed'}>
{Children.map(children, (child) => {
if (isValidElement(child)) {
return cloneElement(child as React.ReactElement<{ itemId?: string; isOpen?: boolean }>, {
itemId: id,
isOpen,
});
}
return child;
})}
</div>
);
}
interface AccordionTriggerProps {
children: ReactNode;
itemId?: string;
isOpen?: boolean;
}
function AccordionTrigger({ children, itemId, isOpen }: AccordionTriggerProps) {
const context = useContext(AccordionContext);
if (!context || !itemId) return null;
return (
<button
className="accordion-trigger"
aria-expanded={isOpen}
onClick={() => context.toggleItem(itemId)}
>
{children}
<span className="icon">{isOpen ? '−' : '+'}</span>
</button>
);
}
interface AccordionContentProps {
children: ReactNode;
isOpen?: boolean;
}
function AccordionContent({ children, isOpen }: AccordionContentProps) {
if (!isOpen) return null;
return <div className="accordion-content">{children}</div>;
}
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;
export { Accordion };
Render Props パターン
基本的な Render Props
import { useState, ReactNode } from 'react';
interface ToggleRenderProps {
on: boolean;
toggle: () => void;
setOn: (value: boolean) => void;
}
interface ToggleProps {
defaultOn?: boolean;
children: (props: ToggleRenderProps) => ReactNode;
}
function Toggle({ defaultOn = false, children }: ToggleProps) {
const [on, setOn] = useState(defaultOn);
const toggle = () => setOn((prev) => !prev);
return <>{children({ on, toggle, setOn })}</>;
}
// Usage
function App() {
return (
<Toggle defaultOn={false}>
{({ on, toggle }) => (
<div>
<p>The toggle is {on ? 'ON' : 'OFF'}</p>
<button onClick={toggle}>Toggle</button>
</div>
)}
</Toggle>
);
}
マウス位置トラッカー
import { useState, useEffect, ReactNode } from 'react';
interface MousePosition {
x: number;
y: number;
}
interface MouseTrackerProps {
children: (position: MousePosition) => ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return <>{children(position)}</>;
}
// Usage
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<div
style={{
position: 'absolute',
left: x + 10,
top: y + 10,
background: 'black',
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
}}
>
{x}, {y}
</div>
)}
</MouseTracker>
);
}
データフェッチ用 Render Props
import { useState, useEffect, ReactNode } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
interface FetchProps<T> {
url: string;
children: (state: FetchState<T>) => ReactNode;
}
function Fetch<T>({ url, children }: FetchProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return <>{children({ data, loading, error, refetch: fetchData })}</>;
}
// Usage
function UserList() {
return (
<Fetch<User[]> url="/api/users">
{({ data, loading, error, refetch }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<button onClick={refetch}>Refresh</button>
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}}
</Fetch>
);
}
ベストプラクティスの概要
| パターン | ユースケース |
|---|---|
| 複合コンポーネント | 暗黙的な状態を共有する関連コンポーネント |
| Render Props | ロジックを共有した動的なレンダリング |
| HOC | 横断的な関心事、コードの再利用 |
| カスタムフック | 状態を持つロジックの再利用 |
| Provider パターン | グローバル/共有状態の管理 |
| 制御/非制御 | フォーム入力の柔軟性 |
| Prop Getter | アクセシブルなコンポーネント API |
| State Reducer | カスタマイズ可能な状態ロジック |
その他の React パターン
残りの6つのパターンの完全なコード例については、references/hoc-hooks-provider-controlled-patterns.mdを参照してください。
- 高階コンポーネント(HOC) — コンポーネントを受け取り、動作を追加した新しいコンポーネントを返す関数です。
- パターンとしてのカスタムフック — 状態を持つロジックを
useXフックにカプセル化します。最新の React では HOC や Render Props よりもよく使われます。 - Provider パターン —
React.createContextと、コンテキストを利用するuseContextフックを組み合わせます。 - 制御コンポーネントと非制御コンポーネント — props によって値を駆動する方式と、ref によって内部状態を駆動する方式です。
- Prop Getter パターン — 利用側に事前設定済みの props を渡す
getProps()コールバックを返します(Downshift、Headless UI で使用されています)。 - State Reducer パターン — 利用側が状態遷移をオーバーライドできるよう、reducer の prop を受け取ります(Kent C. Dodds による標準的な実装)。
コメント
Markdown 対応