·10分で読めます
React でよく使うパターン集
はじめに
React での開発を続けていると、繰り返し使うパターンが見えてきます。この記事では、私が日々の開発でよく使うパターンやテクニックをまとめました。
コンポーネント設計
Compound Components パターン
関連するコンポーネントをグループ化し、柔軟な API を提供するパターンです。
// 使用例
<Card>
<Card.Header>
<Card.Title>タイトル</Card.Title>
</Card.Header>
<Card.Content>本文</Card.Content>
<Card.Footer>フッター</Card.Footer>
</Card>実装例:
function Card({ children }: { children: React.ReactNode }) {
return <div className="rounded-lg border bg-card p-4">{children}</div>;
}
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="mb-4">{children}</div>;
}
function CardTitle({ children }: { children: React.ReactNode }) {
return <h3 className="text-lg font-semibold">{children}</h3>;
}
// 名前空間として結合
Card.Header = CardHeader;
Card.Title = CardTitle;
Card.Content = CardContent;
Card.Footer = CardFooter;
export { Card };Render Props パターン
ロジックを共有しつつ、レンダリングを呼び出し側に委ねるパターンです。
type MousePosition = { x: number; y: number };
function MouseTracker({ children }: { children: (position: MousePosition) => React.ReactNode }) {
const [position, setPosition] = useState({ 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)}</>;
}
// 使用例
<MouseTracker>
{({ x, y }) => (
<p>
マウス位置: ({x}, {y})
</p>
)}
</MouseTracker>;カスタムフック
useLocalStorage
ローカルストレージと同期する状態管理フック。
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
// 使用例
const [theme, setTheme] = useLocalStorage("theme", "light");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;
}
// 使用例
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 300);
useEffect(() => {
// API 呼び出し
search(debouncedSearchTerm);
}, [debouncedSearchTerm]);useMediaQuery
メディアクエリの状態を監視するフック。
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [query]);
return matches;
}
// 使用例
const isMobile = useMediaQuery("(max-width: 768px)");パフォーマンス最適化
useMemo と useCallback
再計算・再生成を防ぐためのメモ化。
// 重い計算のメモ化
const expensiveResult = useMemo(() => {
return items.filter((item) => item.active).map((item) => transform(item));
}, [items]);
// コールバックのメモ化(子コンポーネントへの props として渡す場合)
const handleClick = useCallback((id: string) => {
setSelected(id);
}, []);注意点: 何でもメモ化すれば良いわけではありません。メモ化自体にもコストがあるため、本当に必要な場合のみ使用しましょう。
React.lazy と Suspense
コンポーネントの遅延読み込み。
const HeavyComponent = lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}状態管理パターン
Reducer パターン
複雑な状態遷移を管理する場合に便利。
type State = { count: number; step: number };
type Action = { type: "increment" } | { type: "decrement" } | { type: "setStep"; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "decrement":
return { ...state, count: state.count - state.step };
case "setStep":
return { ...state, step: action.payload };
default:
return state;
}
}
// 使用例
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });型安全なパターン
Discriminated Union
型の絞り込みを活用したパターン。
type Result<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function DataDisplay({ result }: { result: Result<User[]> }) {
switch (result.status) {
case "loading":
return <Spinner />;
case "error":
return <ErrorMessage error={result.error} />;
case "success":
return <UserList users={result.data} />;
}
}Generic Components
再利用可能な型安全コンポーネント。
type SelectProps<T> = {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
};
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={getValue(value)}
onChange={(e) => {
const selected = options.find((opt) => getValue(opt) === e.target.value);
if (selected) onChange(selected);
}}
>
{options.map((option) => (
<option key={getValue(option)} value={getValue(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}まとめ
これらのパターンは、状況に応じて使い分けることが大切です。シンプルな解決策で十分な場合は、無理にパターンを適用する必要はありません。
コードの可読性と保守性を意識しながら、適切なパターンを選択していきましょう。