useEffect

useEffect で外部システムと同期する方法、依存配列、cleanup の役割を学ぶ。

useEffect は、React の外部にある仕組みとコンポーネントを同期するための Hook です。ブラウザ API、イベント購読、タイマー、WebSocket 接続などを扱うときに使います。

Effect が必要かを先に確かめる

Effect は「レンダーによって発生する副作用」を扱う手段です。しかし、すべての処理を Effect に入れる必要はありません。

  • props や state から計算できる値は、レンダー中に計算する
  • クリックなどの操作への反応は、イベントハンドラで扱う
  • React の外部システムとの同期には、Effect を使う

たとえば、完了済みタスク数を state にコピーして Effect で更新する必要はありません。一方、ブラウザのウィンドウ幅を購読する処理は React の外部との同期なので Effect が適しています。

基本形と依存配列

次の Effect は、コンポーネントが表示された後にリサイズイベントを購読します。

import { useEffect, useState } from 'react';

function WindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const updateWidth = () => setWidth(window.innerWidth);
    updateWidth();
    window.addEventListener('resize', updateWidth);

    return () => window.removeEventListener('resize', updateWidth);
  }, []);

  return <p>画面幅: {width}px</p>;
}

第2引数の依存配列には、Effect 内で参照し、コンポーネントの外部から変化しうる値を並べます。[] は依存値がないことを表し、通常は表示後に一度だけ実行されます。依存値が変わる場合は、新しい値で同期し直します。

cleanup で後始末をする

Effect から関数を返すと cleanup になります。cleanup はコンポーネントが画面から外れるときだけでなく、依存値の変更によって Effect を再実行する前にも呼ばれます。

イベントリスナー、タイマー、接続のように開始と終了が対になる処理では、開始した場所で必ず cleanup を返します。

useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();

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

開発時には、Effect が setup → cleanup → setup と実行されることがあります。これは cleanup が正しく実装されているかを確かめるためで、cleanup 後に再接続しても正しく動くように書くのが大切です。

非同期処理で古い結果を反映しない

Effect 内で通信する場合、依存値が変わった後に古いリクエストが完了することがあります。cleanup で「この Effect は無効になった」と記録し、古い結果で state を更新しないようにします。

useEffect(() => {
  let cancelled = false;

  async function loadUser() {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    if (!cancelled) setUser(user);
  }

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

実際のアプリケーションでは、フレームワークのデータ取得機構やキャッシュライブラリを使う選択肢もあります。useEffect は外部同期の基本を理解するための重要な API ですが、データ取得のすべてを単独で担うものではありません。

依存配列は実装の一覧ではなく同期条件

依存配列は、Effect が「どの値が変わったら再同期すべきか」を表します。依存配列を空にして警告を抑えるのではなく、Effect が参照する reactive な値を正しく列挙します。

useEffect(() => {
  const connection = createConnection(serverUrl, roomId);
  connection.connect();
  return () => connection.disconnect();
}, [serverUrl, roomId]);

roomId を依存配列から外すと、部屋を切り替えても古い接続が残ります。逆に、レンダーごとに新しく作られるオブジェクトや関数を依存に含めると、不要に再同期することがあります。その場合は値を Effect の内側で作る、または設計を分けることを検討します。

UI を見せる前に処理が必要な場合

通常の Effect は React が画面を反映した後に実行されます。そのため、位置計測など、表示前に行わないとちらつきが出る視覚的な処理には useLayoutEffect が必要になることがあります。ただし描画を待たせるため、まずは通常の useEffect で問題がないかを確認します。

通信を中断する方法

cancelled フラグに加え、AbortController で不要になったリクエスト自体を中断できます。

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then((response) => response.json())
    .then(setUser)
    .catch((error) => {
      if (error.name !== 'AbortError') throw error;
    });

  return () => controller.abort();
}, [userId]);

エラー表示、再試行、キャッシュ、サーバーでのデータ取得まで必要になると、手作業の Effect はすぐ複雑になります。その段階では採用しているフレームワークのデータ取得機能や、責務を閉じ込めたカスタム Hook を選ぶのが実践的です。

Effect を減らすチェック

Effect を書く前に、次を確認します。

  • 現在の props と state から計算できないか
  • ユーザー操作のハンドラに置けないか
  • 親から渡された値をそのまま使えないか
  • 本当に外部システムとの同期なのか

Effect を少なく保つほど、実行順や依存値を追う負担も小さくなります。

Sources