🎉

React公式ドキュメントに学ぶ:useEffectを減らす実践的アプローチ

に公開

React公式ドキュメントの「そのエフェクトは不要かも」は、useEffectの過剰使用を避けるための重要な指針を提供しています。本記事では、公式ドキュメントの具体例を通じて、より良いReactコードを書くための実践的なアプローチを解説します。

なぜuseEffectを減らすべきか

useEffectは強力なツールですが、過剰に使用すると以下の問題が発生します:

  • 不要な再実行: 依存関係の変更により予期しないタイミングで実行される
  • パフォーマンスの低下: 不必要な処理やレンダリングが発生
  • コードの複雑化: データフローが追いにくくなる
  • バグの温床: タイミングに依存したバグが発生しやすい

実践例:useEffectを使わないパターン

1. イベントハンドラーへの移動

// 問題のあるコード
function Form() {
  const [message, setMessage] = useState('');

  // ❌ メッセージが変更されるたびに送信される
  useEffect(() => {
    sendMessage(message);
  }, [message]);

  return (
    <form>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </form>
  );
}

// 改善されたコード
function Form() {
  const [message, setMessage] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    // ✅ フォーム送信時のみ実行される
    sendMessage(message);
    setMessage('');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={message} onChange={e => setMessage(e.target.value)} />
      <button type="submit">送信</button>
    </form>
  );
}

ポイント: ユーザーアクションに対する処理は、useEffectではなくイベントハンドラーで処理する

2. 計算結果のキャッシュ(useMemoの活用)

// 問題のあるコード
function TodoList({ todos, filter }) {
  const [visibleTodos, setVisibleTodos] = useState([]);

  // ❌ todosやfilterが変更されるたびに実行される
  useEffect(() => {
    setVisibleTodos(todos.filter(todo => todo.status === filter));
  }, [todos, filter]);

  return <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}

// 改善されたコード
function TodoList({ todos, filter }) {
  // ✅ 計算結果を直接メモ化
  const visibleTodos = useMemo(
    () => todos.filter(todo => todo.status === filter),
    [todos, filter]
  );

  return <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}

ポイント: 派生状態の計算にはuseEffectではなくuseMemoを使用する

3. keyを使ったstateのリセット

// 問題のあるコード
function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // ❌ userIdが変更されるたびにコメントをリセット
  useEffect(() => {
    setComment('');
  }, [userId]);

  return <CommentForm comment={comment} onChange={setComment} />;
}

// 改善されたコード
function ProfilePage({ userId }) {
  return (
    // ✅ keyが変更されると、コンポーネント全体が再作成される
    <CommentForm key={userId} />
  );
}

function CommentForm() {
  const [comment, setComment] = useState('');
  return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}

ポイント: propsの変更に応じてstateをリセットしたい場合は、keyを使ってコンポーネントインスタンスを制御する

4. 非同期処理の適切な配置

// 問題のあるコード
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // ❌ 複雑な非同期ロジックがuseEffect内に散在
    let cancelled = false;
    setLoading(true);

    searchAPI(query).then(data => {
      if (!cancelled) {
        setResults(data);
        setLoading(false);
      }
    });

    return () => { cancelled = true; };
  }, [query]);

  return loading ? <Spinner /> : <ResultsList results={results} />;
}

// 改善されたコード(React Queryパターン)
function SearchResults({ query }) {
  // ✅ データフェッチングライブラリを活用
  const { data: results, isLoading } = useQuery({
    queryKey: ['search', query],
    queryFn: () => searchAPI(query),
    enabled: !!query
  });

  return isLoading ? <Spinner /> : <ResultsList results={results} />;
}

ポイント: データフェッチングにはReact QueryやSWRなどの専用ライブラリを活用する

useEffectが本当に必要な場合

以下のケースではuseEffectの使用が適切です:

  1. 外部システムとの同期
  • WebSocket接続の管理
  • ブラウザAPIの購読(例:window.addEventListenerなど)
  • サードパーティライブラリの初期化
  1. 分析やロギング
  • ページビューの記録
  • パフォーマンス計測
function ChatRoom({ roomId }) {
  useEffect(() => {
    // ✅ 外部システムとの同期には適切
    const connection = createConnection(roomId);
    connection.connect();

    return () => connection.disconnect();
  }, [roomId]);
}

まとめ:useEffectを減らすための指針

  1. イベントに反応する処理 → イベントハンドラーで処理
  2. propsやstateから計算できる値 → useMemoやレンダリング中に計算
  3. propsの変更でstateをリセット → key属性を活用
  4. データフェッチング → 専用ライブラリを使用
  5. 外部システムとの同期 → useEffectを使用(これは適切)

これらのパターンを理解し適用することで、より予測可能で保守しやすいReactコードを書くことができます。useEffectは強力なツールですが、「本当に必要か?」を常に問いかけることが重要です。

参考

https://ja.react.dev/learn/you-might-not-need-an-effect

Discussion