useEffectの使い方

テクノロジー

useEffectって便利ですよね。

ですが乱用はすべきじゃないです。

useEffect の本質的な仕組みから、なぜ乱用を避けるべきなのか、そして具体的なアンチパターンまで、分かりやすく解説しようと思います。

1. useEffectの仕組み

useEffect を正しく使うために、その実行タイミングと仕組みを理解しておきましょう。

ポイントは、**「レンダリングが完了し、画面に反映された『後』に非同期で実行される」**という点です。

実行の流れ

1. コンポーネントの実行(レンダー): JSXから仮想DOMが作られます。

2. ブラウザへの描画(コミット): 画面が実際に更新されます。

3. Effectの実行: ここで初めて useEffect の中身が動きます。

依存配列(Dependencies)による制御

useEffect の第2引数に渡す配列によって、実行されるタイミングをコントロールします。

// ① 毎回のレンダリング後に実行(危険・ほぼ使わない)
useEffect(() => {
console.log('毎回実行されます');
});

// ② マウント時(初回表示時)のみ実行
useEffect(() => {
  console.log('初回だけ実行されます');
}, []);

// ③ 特定のデータが変更されたときだけ実行
useEffect(() => {
  console.log('countが変わったときだけ実行されます');
}, [count]);

クリーンアップ関数の重要性

useEffect の中で return () => { ... } と関数を返すと、それはクリーンアップ関数になります。

コンポーネントが消える(アンマウント)直前や、次のエフェクトが実行される直前に動き、リスナーの解除やタイマーのクリアなど、後片付けを行います。

2. なぜuseEffectの乱用を避けるべきなのか?

簡単に話すとuseEffect を乱用すると**「アプリの動作が重くなり、コードの予測可能性が著しく低下するから」**です。具体的には以下の3つの問題が発生します。

無駄なレンダリング(再描画)の発生: useEffect の中でステート(setState)を更新すると、画面が描画された直後にもう一度レンダリングが走ります。これが連鎖すると、パフォーマンスが急激に低下します。

バグの温床(無限ループなど): 依存配列(deps)の管理を誤ると、処理が無限に実行され続け、ブラウザがフリーズする原因になります。

コードの可読性の低下: 「どこでデータが変わったのか」の追跡が難しくなり、いわゆる「スパゲティコード」化しやすくなります。

3. useEffectのアンチパターン(よくある誤用例)

開発現場で見かける、useEffect を「使うべきではない」代表的な4つのケースと、その解決策です。

アンチパターン①:PropsやStateから新しい値を計算する

❌ 悪い例

苗字(lastName)と名前(firstName)が変わったから、フルネームを useEffect で合成してステートに入れる。

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

// ✕ 無駄な再レンダリングが発生する
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);⭕ 改善案

コンポーネントの実行中に直接計算すれば十分です。ステートも useEffect も不要になります。

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ⭕ レンダー時に計算(必要ならuseMemoを使う)
const fullName = `${firstName} ${lastName}`;

アンチパターン②:ユーザーの操作(イベント)を起点に処理する

❌ 悪い例

「ボタンを押した」というフラグを検知して、useEffect でお祝いのアラートを出す。

const [isPurchased, setIsPurchased] = useState(false);

// ✕ ユーザー操作の結果なのに、エフェクトで検知している
useEffect(() => {
  if (isPurchased) {
    showAlert('ご購入ありがとうございます!');
  }
}, [isPurchased]);

⭕ 改善案

特定のボタンクリックなど、ユーザーの操作が起点となる処理は、イベントハンドラーの中に直接書くべきです。

const handlePurchase = () => {
  setIsPurchased(true);
  showAlert('ご購入ありがとうございます!'); // ⭕ ここで実行する
};

アンチパターン③:Propsの変更に合わせてStateをリセットする

❌ 悪い例

ブログ記事(postId)が切り替わったので、コメント入力欄(commentText)を空っぽにリセットしたい。

// ✕ 一瞬古いコメントが残ったまま描画され、そのあとリセットされる(チカチカする原因)
useEffect(() => {
  setCommentText('');
}, [postId]);

⭕ 改善案

Reactの key 属性を利用します。key が変わると、Reactはそのコンポーネントを完全に破棄して初期状態から作り直してくれます。

// 親コンポーネント側で key を指定する
<CommentForm key={postId} postId={postId} />