← 🏠 記事一覧に戻る
📅📁frontend🏷️

📝 React Hooksのベストプラクティス

useEffectの依存配列やカスタムフックの実装など、実践的なHooksの使い方を解説します

React Hooksのベストプラクティス

React Hooksが導入されてから、コンポーネントの状態管理や副作用の処理が大きく変わりました。今回は実際の開発で役立つHooksのベストプラクティスを紹介します。

useEffectの依存配列を正しく管理する

useEffectの依存配列は、Reactの核心的な概念の一つです。正しく理解することで、不要な再レンダリングやメモリリークを防げます。

避けるべきパターン

// ❌ 依存配列を空にしすぎる
useEffect(() => {
  fetchUserData(userId);
}, []); // userIdが変わっても実行されない

// ❌ 依存配列を省略する
useEffect(() => {
  setCount(count + 1);
}); // 無限ループの原因

推奨パターン

// ✅ 必要な依存関係を正しく指定
useEffect(() => {
  const fetchData = async () => {
    const data = await fetchUserData(userId);
    setUserData(data);
  };
  
  fetchData();
}, [userId]); // userIdが変わった時のみ実行

// ✅ useCallbackで関数をメモ化
const handleSubmit = useCallback((formData) => {
  submitForm(formData);
}, []);

useEffect(() => {
  handleSubmit(data);
}, [data, handleSubmit]);

カスタムフックで共通ロジックを抽出

繰り返し使用されるロジックは、カスタムフックとして抽出することで再利用性が向上します。

APIリクエストのカスタムフック

interface UseApiState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

export const useApi = <T>(url: string): UseApiState<T> => {
  const [state, setState] = useState<UseApiState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        setState(prev => ({ ...prev, loading: true, error: null }));
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error('API request failed');
        }
        
        const data = await response.json();
        setState({ data, loading: false, error: null });
      } catch (error) {
        setState({
          data: null,
          loading: false,
          error: error instanceof Error ? error.message : 'Unknown error',
        });
      }
    };

    fetchData();
  }, [url]);

  return state;
};

使用例

const UserProfile = ({ userId }: { userId: string }) => {
  const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

useMemoとuseCallbackの適切な使用

パフォーマンス最適化のためのフックですが、過度な使用は逆効果になることもあります。

useMemoの適切な使用

const ExpensiveComponent = ({ items, filter }: Props) => {
  // ✅ 計算コストの高い処理をメモ化
  const filteredItems = useMemo(() => {
    return items.filter(item => {
      // 複雑なフィルタリングロジック
      return performExpensiveFiltering(item, filter);
    });
  }, [items, filter]);

  // ❌ 単純な計算はメモ化不要
  const simpleCount = useMemo(() => items.length, [items]);

  return (
    <div>
      {filteredItems.map(item => (
        <Item key={item.id} item={item} />
      ))}
    </div>
  );
};

useReducerで複雑な状態管理

複数の状態が相互に関連する場合は、useReducerを使用することで状態管理がシンプルになります。

interface FormState {
  values: Record<string, string>;
  errors: Record<string, string>;
  isSubmitting: boolean;
}

type FormAction =
  | { type: 'SET_FIELD'; field: string; value: string }
  | { type: 'SET_ERROR'; field: string; error: string }
  | { type: 'CLEAR_ERRORS' }
  | { type: 'SET_SUBMITTING'; isSubmitting: boolean };

const formReducer = (state: FormState, action: FormAction): FormState => {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value },
        errors: { ...state.errors, [action.field]: '' },
      };
    case 'SET_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: action.error },
      };
    case 'CLEAR_ERRORS':
      return { ...state, errors: {} };
    case 'SET_SUBMITTING':
      return { ...state, isSubmitting: action.isSubmitting };
    default:
      return state;
  }
};

export const useForm = (initialValues: Record<string, string>) => {
  const [state, dispatch] = useReducer(formReducer, {
    values: initialValues,
    errors: {},
    isSubmitting: false,
  });

  const setField = useCallback((field: string, value: string) => {
    dispatch({ type: 'SET_FIELD', field, value });
  }, []);

  const setError = useCallback((field: string, error: string) => {
    dispatch({ type: 'SET_ERROR', field, error });
  }, []);

  return { state, setField, setError, dispatch };
};

まとめ

React Hooksを効果的に使用するためのポイント:

  1. 依存配列を正しく管理し、不要な再実行を防ぐ
  2. カスタムフックで共通ロジックを再利用可能にする
  3. useMemo/useCallbackは本当に必要な場面でのみ使用する
  4. useReducerで複雑な状態管理をシンプルにする

これらのベストプラクティスを意識することで、より保守性が高く、パフォーマンスの良いReactアプリケーションを構築できます。