· yuta

React useEffectとは何者なのかを理解する

#react#typescript#frontend

Reactを書いていると必ず出会うのが useEffect です。API通信やタイマーを扱うときに使うことは何となく分かっていても、「そもそも何をする関数なのか」「なぜ必要なのか」が曖昧なまま使いがちです。この記事では、useEffect が何者なのかを、その背景にある「副作用」という考え方から整理しておきます。

例として、架空のオンライン料理教室アプリ CookMate を想定します。/lessons/:lessonId/ というURLでレッスン詳細ページを開く、という場面を使います。

そもそも「副作用」とは何か

useEffect を理解するには、先に 副作用(side effect) という言葉を押さえる必要があります。

Reactコンポーネントの本来の仕事は、たった一つです。

データ(props / state)を受け取って、画面(JSX)を返す。

この「本来の仕事」以外の処理を、まとめて 副作用 と呼びます。たとえば次のようなものです。

これらは「画面を返す」という純粋な計算の外側で、外の世界に触れる処理です。そして、こうした副作用をコンポーネントの本体に直接書いてはいけません。

なぜ本体に直接書いてはいけないのか

コンポーネントの関数本体は、再描画(レンダリング)のたびに何度も実行されます。もしAPI通信を本体に直接書くと、こうなります。

export function LessonPage() {
  const [lesson, setLesson] = useState(null);

  // ❌ これはダメ
  fetchLesson(lessonId).then(setLesson);
  // setLesson で state が変わる → 再描画 → また fetch → また setLesson → ...

  return <div>{lesson?.title}</div>;
}

fetch → setLesson で state が変わり、再描画され、また fetch が走り……と無限ループになってしまいます。

副作用は「描画のたび」ではなく「必要なタイミングだけ」実行したい。そのための仕組みが useEffect です。

useEffectの基本

useEffect は、描画が終わった後に副作用を実行するための関数です。基本形はこうです。

useEffect(() => {
  // ここに副作用を書く(描画の後に実行される)
}, [依存する値]);

引数は2つあります。

  1. 実行したい処理(第1引数の関数)
  2. 依存配列(第2引数の [])… 「この中の値が変わったときだけ実行する」というトリガー指定

依存配列で実行タイミングを決める

useEffect の挙動は、第2引数の依存配列で決まります。ここが最重要ポイントです。

書き方 いつ実行されるか
useEffect(fn, []) 最初の1回だけ(マウント時)
useEffect(fn, [a, b]) 最初 + a か b が変わるたび
useEffect(fn)(配列を省略) 毎回の描画後(基本的に使わない)

「最初の1回だけ実行したい」なら空配列 []、「特定の値が変わったら実行し直したい」なら、その値を配列に入れる、と覚えておけば十分です。

実例:レッスン情報を取得する

CookMateのレッスン詳細ページで、URLの lessonId をもとにレッスン情報を取得してみます。

export function LessonPage({ lessonId }: { lessonId: string }) {
  const [lesson, setLesson] = useState<Lesson | null>(null);

  useEffect(() => {
    fetchLesson(lessonId).then(setLesson);
  }, [lessonId]); // lessonId が変わったら取得し直す

  if (!lesson) return <p>読み込み中...</p>;
  return <h1>{lesson.title}</h1>;
}

このコードはこう読みます。

「lessonId が決まった(あるいは変わった)タイミングで、レッスン情報を取得する」

この「まず画面を出す → 裏でデータを取る → 届いたら再描画」という流れこそ、useEffect が副作用を「描画の後」に実行してくれるおかげで成り立っています。

描画(まだデータ無し → 「読み込み中...」)
   ↓ useEffect 発火
API通信
   ↓ データ到着(setLesson)
再描画(タイトルを表示)

クリーンアップ:後片付けの仕組み

副作用の中には、後片付けが必要なものがあります。タイマーやイベント登録は、放置するとコンポーネントが消えた後も動き続け、メモリリークの原因になります。

useEffect の第1引数の関数から関数を return すると、それが「後片付け(クリーンアップ)」として実行されます。

useEffect(() => {
  const timer = setInterval(() => {
    console.log('1秒ごとに実行');
  }, 1000);

  // return した関数が後片付けとして呼ばれる
  return () => clearInterval(timer);
}, []);

クリーンアップが呼ばれるのは、次のタイミングです。

API通信だけなら不要なことが多いですが、タイマー・購読・イベントリスナーを扱うときは必ず後片付けをセットで書く、と覚えておくと安全です。

使うときの注意点

1. 依存配列を正しく書く

useEffect の中で使っている値(state、props、関数など)は、原則すべて依存配列に入れます。入れ忘れると「古い値を参照し続ける」バグの温床になります。ReactのESLintルール(react-hooks/exhaustive-deps)が警告してくれるので、基本はそれに従うのが安全です。

2. なんでも useEffect にしない

「props から計算できるだけの値」に useEffect は不要です。副作用ではなく、ただの計算だからです。

// ❌ 不要な useEffect
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// ✅ 描画時に計算するだけでよい
const fullName = `${firstName} ${lastName}`;

useEffect は「外の世界に触れる処理(副作用)」のためのものであって、値の計算のためのものではありません。ここを混同すると、無駄な再描画や複雑なコードにつながります。

3. 描画の後に実行される、を忘れない

useEffect の中身は、コンポーネントが一度描画された後に実行されます。そのため「最初の描画時点ではまだデータが無い」状態が必ず存在します。前述の「読み込み中…」のような初期表示を用意しておくのが定石です。

まとめ

useEffect は「描画」と「副作用」を切り分けるための仕組みだと捉えると、いつ使うべきかが見えてきます。まずは「これは画面を返す仕事か、それとも外の世界に触れる仕事か」を意識するところから始めるとよいです。