React

パターン

React

カスタムフック・コンポジション・Render Props

カスタムフック

ロジックを再利用可能なフックに抽出

useFetch.ts tsx
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

hooks.ts ts
// 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

子コンポーネントのエラーをキャッチ

ErrorBoundary.tsx tsx
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>