react-patterns について
React 開発者向けのコンポーネントパターンをまとめたスキルです。複合コンポーネント、Render Props、HOC、カスタムフック、コンポジションと、それぞれの用途や注意点を扱います。
主な機能
- 01複合コンポーネントパターン
- 02Render Props と HOC
- 03カスタムフック
- 04コンポーネントのコンポジション
- 05パターン選択と問題の調査
活用シーン
- 01関連コンポーネントを柔軟に構築
- 02状態を持つロジックを再利用
- 03コンポーネントのロジックと UI を分離
- 04Context や描画の問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-patternsインストール後、Claude Code で/react-patternsを入力して読み込まれたか確認します。
同梱ファイルadvanced.md
詳細
React パターン
完全なリファレンス:Container/Presentational、State Machine、Slots、Polymorphic、Provider のパターンについては、advanced.md を参照してください。
Compound Components
連携して一貫性のある UI を構成するコンポーネントです。
import { createContext, useContext, useState, ReactNode } from 'react';
interface AccordionContextValue {
openItems: Set<string>;
toggle: (id: string) => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within Accordion');
}
return context;
}
function Accordion({ children, multiple = false, defaultOpen = [] }: AccordionProps) {
const [openItems, setOpenItems] = useState<Set<string>>(new Set(defaultOpen));
const toggle = (id: string) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!multiple) next.clear();
next.add(id);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
// Attach sub-components
Accordion.Item = Item;
Accordion.Trigger = Trigger;
Accordion.Content = Content;
Render Props
レンダリングを制御するために、関数を children または props として渡します。
interface DataFetcherProps<T> {
url: string;
children: (state: {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}) => ReactNode;
}
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch');
setData(await res.json());
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => { fetchData(); }, [fetchData]);
return <>{children({ data, loading, error, refetch: fetchData })}</>;
}
// Usage
<DataFetcher<User[]> url="/api/users">
{({ data, loading, error, refetch }) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
return <ul>{data?.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}}
</DataFetcher>
Higher-Order Components(HOC)
機能を追加するために、コンポーネントをラップします。
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>,
LoadingComponent: React.ComponentType = DefaultSpinner
) {
return function WithLoadingComponent(props: P & { loading: boolean }) {
const { loading, ...restProps } = props;
if (loading) return <LoadingComponent />;
return <WrappedComponent {...(restProps as P)} />;
};
}
// HOC with Refs
function withLogger<P extends object>(WrappedComponent: React.ComponentType<P>) {
const WithLogger = React.forwardRef<unknown, P>((props, ref) => {
useEffect(() => { console.log('Props changed:', props); }, [props]);
return <WrappedComponent {...props} ref={ref} />;
});
WithLogger.displayName = `WithLogger(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithLogger;
}
Custom Hooks パターン
再利用可能なロジックをフックに切り出します。
// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch { return initialValue; }
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
// useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// useOnClickOutside
function useOnClickOutside(
ref: RefObject<HTMLElement>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) return;
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
Composition パターン
継承よりもコンポジションを優先します。
function Card({ children, className }: CardProps) {
return <div className={`card ${className || ''}`}>{children}</div>;
}
function CardHeader({ children }: { children: ReactNode }) {
return <div className="card-header">{children}</div>;
}
function CardBody({ children }: { children: ReactNode }) {
return <div className="card-body">{children}</div>;
}
// Specialized cards through composition
function UserCard({ user }: { user: User }) {
return (
<Card>
<CardHeader>
<Avatar src={user.avatar} />
<h3>{user.name}</h3>
</CardHeader>
<CardBody><p>{user.bio}</p></CardBody>
</Card>
);
}
ベストプラクティスのまとめ
| パターン | 使用する場面 |
|---|---|
| Compound Components | 柔軟性の高い、関連するコンポーネント |
| Render Props | 振る舞いを共有し、レンダリングを制御する場合 |
| HOC | 横断的な関心事、レガシーコード |
| Custom Hooks | 再利用可能な状態を持つロジック |
| Composition | 単純な要素から複雑な要素を構築する場合 |
| Container/Presentational | ロジックと UI を分離する場合 |
| State Machine | 複雑な状態遷移 |
| Polymorphic | 柔軟な要素の種類 |
- 再利用可能なロジックにはカスタムフックを使用します
- コンポーネントは小さく、役割を明確に保ちます
- DX を向上させるために TypeScript を使用します
- パターンを過剰に使用しないでください。KISS を心がけます
- HOC を過度にネストしないでください
アンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
| 単純なコンポーネントの過剰な設計 | 不必要な複雑さ | シンプルな props を使用する |
| HOC の過剰なネスト | props の衝突、デバッグの困難さ | コンポジションまたはフックを使用する |
| メモ化しない Render Props | パフォーマンスの問題 | レンダー関数をメモ化する |
| 深い階層の prop drilling | 保守が困難 | Context または Compound Components を使用する |
| 頻繁に変化する値に Context を使用する | パフォーマンスの問題 | 状態管理ライブラリを使用する |
トラブルシューティングのクイックガイド
| 問題 | 考えられる原因 | 修正方法 |
|---|---|---|
| Context を利用できない | Provider がない | Provider コンポーネントでラップする |
| HOC で ref が失われる | ref が転送されていない | React.forwardRef を使用する |
| Render Props による再レンダリング | 関数の同一性が変化する | レンダー関数をメモ化する |
リファレンスドキュメント
コメント
Markdown 対応