パターン
React
カスタムフック・コンポジション・Render Props
カスタムフック
ロジックを再利用可能なフックに抽出
import { useState, useEffect, useCallback } from 'react';
// データフェッチ汎用フック
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const refetch = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setData(await res.json());
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => { refetch(); }, [refetch]);
return { data, loading, error, refetch };
}
// 使用
function UserPage({ id }) {
const { data: user, loading, error } = useFetch<User>(`/api/users/${id}`);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserCard user={user!} />;
}useLocalStorage / useDebounce
// useLocalStorage: 状態を localStorage に永続化
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initial;
} catch { return initial; }
});
const set = useCallback((v: T | ((prev: T) => T)) => {
setValue(prev => {
const next = typeof v === 'function' ? (v as Function)(prev) : v;
localStorage.setItem(key, JSON.stringify(next));
return next;
});
}, [key]);
return [value, set] as const;
}
// useDebounce: 値の更新を遅延
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
// 使用: 検索入力のデバウンス
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 400);
useEffect(() => { search(debouncedQuery); }, [debouncedQuery]);Error Boundary
子コンポーネントのエラーをキャッチ
import { Component, type ReactNode, type ErrorInfo } from 'react';
interface Props {
fallback: ReactNode | ((error: Error) => ReactNode);
children: ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
}
interface State { error: Error | null; }
// Error Boundary はクラスコンポーネントが必要
export class ErrorBoundary extends Component<Props, State> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
// Sentry.captureException(error); など
}
render() {
if (this.state.error) {
const { fallback } = this.props;
return typeof fallback === 'function'
? fallback(this.state.error)
: fallback;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<p>エラーが発生しました</p>}>
<UserDashboard />
</ErrorBoundary>