React初心者必見!useEffectとuseLayoutEffectの違いを徹底解説

Reactの開発において、useEffectuseLayoutEffectは、状態管理や副作用の処理を行う上で欠かせないフックです。しかし、それぞれがどのように異なり、どのような場面で使い分けるべきかについて、初心者には難しいと感じるかもしれません。本記事では、両者の基本的な概要と違いから、具体的な使用例、適切な使い分け方、そしてよくある間違いまで、詳しく解説します。この内容を学ぶことで、Reactコンポーネントのパフォーマンスを向上させ、より理解の深いコードが書けるようになるでしょう。

目次

Reactにおけるフックとは


Reactのフックは、関数コンポーネントで状態やライフサイクル機能を利用するための仕組みです。それまでクラスコンポーネントでしか使えなかった機能を関数コンポーネントでも利用できるようになり、コードが簡潔で読みやすくなる利点があります。

代表的なフック


Reactにはさまざまなフックがありますが、以下はその中でも代表的なものです。

useState


コンポーネントの状態を管理するためのフックです。状態の初期値を設定し、状態を更新する関数を返します。

useEffect


副作用(サイドエフェクト)を処理するためのフックです。データのフェッチやDOMの操作などに使われます。

useContext


ReactのコンテキストAPIと組み合わせて、コンテキストにアクセスするためのフックです。

useEffectとuseLayoutEffectの位置付け


特にuseEffectとuseLayoutEffectは、コンポーネントのライフサイクルにおける副作用の処理に特化しています。しかし、それぞれの処理タイミングや目的が異なるため、使い分けが重要です。次のセクションから、この2つのフックの特徴を掘り下げていきます。

useEffectの概要と使用例

useEffectとは


useEffectは、Reactコンポーネントで副作用を処理するためのフックです。副作用とは、レンダリング以外で発生する動作のことで、以下のような処理が該当します。

  • データのフェッチ(APIリクエスト)
  • DOMの操作
  • タイマーの設定やクリーンアップ

useEffectは、コンポーネントがレンダリングされた後に非同期的に実行されます。これにより、UIの描画に影響を与えず、副作用処理が可能になります。

基本的な使い方


以下のコードは、コンポーネントがマウントされた際にAPIからデータを取得する例です。

import React, { useEffect, useState } from 'react';

function FetchDataComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // データをフェッチする非同期関数
    async function fetchData() {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();
      setData(result);
    }

    fetchData();
  }, []); // 依存配列が空の場合、初回レンダリング時のみ実行

  return (
    <div>
      <h1>データ:</h1>
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'ロード中...'}
    </div>
  );
}

依存配列について


useEffectの第2引数には「依存配列」と呼ばれる配列を指定できます。これにより、特定の変数が変更された時にのみ、useEffectが再実行されるように制御できます。

  • 空の配列 []: 初回レンダリング時のみ実行されます。
  • 特定の値を指定 [value]: valueが変更された時にのみ実行されます。
  • 指定なし: コンポーネントのレンダリングごとに実行されます。

クリーンアップ処理


useEffect内でイベントリスナーの登録やタイマーを設定した場合、コンポーネントがアンマウントされる際にクリーンアップが必要です。そのため、useEffectではクリーンアップ関数を返すことが推奨されています。

useEffect(() => {
  const timer = setInterval(() => {
    console.log('タイマー動作中');
  }, 1000);

  return () => {
    clearInterval(timer); // タイマーのクリーンアップ
    console.log('クリーンアップ完了');
  };
}, []);

useEffectを正しく理解し、適切に使用することで、Reactコンポーネントの動作を効率的に管理できます。

useLayoutEffectの概要と使用例

useLayoutEffectとは


useLayoutEffectは、Reactで副作用を処理するためのもう一つの重要なフックです。基本的な目的はuseEffectと同様ですが、主な違いはその実行タイミングにあります。useLayoutEffectは、DOMの更新後、描画(ブラウザによる画面表示)の前に同期的に実行されます。このため、DOMを操作してユーザーに表示される前に状態を調整する場合に使用します。

useEffectとの違い

  • useEffect: 非同期的に実行されるため、描画が完了した後に実行されます。
  • useLayoutEffect: 同期的に実行されるため、描画前にDOMを操作できます。

基本的な使い方


以下は、DOMのサイズを計測し、それを基に状態を設定する例です。

import React, { useLayoutEffect, useRef, useState } from 'react';

function LayoutEffectExample() {
  const [boxWidth, setBoxWidth] = useState(0);
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    // DOM要素の幅を取得
    if (boxRef.current) {
      setBoxWidth(boxRef.current.offsetWidth);
    }
  }, []); // 初回レンダリング時のみ実行

  return (
    <div>
      <div
        ref={boxRef}
        style={{ width: '50%', height: '100px', backgroundColor: 'lightblue' }}
      >
        ボックス
      </div>
      <p>ボックスの幅: {boxWidth}px</p>
    </div>
  );
}

この例では、DOM要素の幅を取得し、その情報をReactの状態として保存しています。useLayoutEffectを使用することで、描画前に幅を正確に取得できます。

useLayoutEffectを使うべきケース


useLayoutEffectは、以下のような場面で有効です。

  • DOMを操作して描画内容を変更したい場合
    例えば、要素のスタイルやサイズを設定する際に、ブラウザが間違った情報を一瞬表示してしまう「フラッシュ」を防ぐことができます。
  • 計算結果に基づきレイアウトを変更する場合
    DOMのサイズや位置を計算し、それに応じたレイアウト調整が必要な場合に便利です。

注意点

  • パフォーマンスへの影響
    useLayoutEffectは同期的に実行されるため、処理が重いとレンダリングの遅延を引き起こす可能性があります。必要最小限の使用にとどめるべきです。
  • 無闇な使用は避ける
    多くの場面ではuseEffectで十分です。特にDOM操作が必要ない場合や非同期で問題ない場合には、useEffectを選択してください。

クリーンアップ処理


useLayoutEffectでもuseEffect同様にクリーンアップ処理を行えます。タイマーやイベントリスナーを登録する場合には、以下のようにクリーンアップを実装します。

useLayoutEffect(() => {
  const resizeObserver = new ResizeObserver((entries) => {
    console.log('サイズ変更検出');
  });
  resizeObserver.observe(document.body);

  return () => {
    resizeObserver.disconnect(); // クリーンアップ
    console.log('オブザーバー解除');
  };
}, []);

useLayoutEffectは特定のユースケースで強力な力を発揮しますが、使いすぎに注意し、目的に応じて正しいフックを選びましょう。

両者の主な違い

実行タイミングの違い


useEffectとuseLayoutEffectの最大の違いは、副作用が実行されるタイミングです。

useEffect

  • DOMの更新後、ブラウザが描画を完了した後に実行されます。
  • 非同期的に動作するため、UIスレッドをブロックしません。
  • 主にAPI呼び出しやロギング、非同期操作に適しています。

useLayoutEffect

  • DOMの更新後、ブラウザが描画する前に同期的に実行されます。
  • DOM操作やスタイルの変更が必要な場面で使用されます。
  • UIスレッドをブロックするため、パフォーマンスに影響を与える可能性があります。

使うべき場面

useEffectを選ぶべき場合

  • 非同期処理が主な目的の場合
    データのフェッチやサーバー通信など、DOMの状態に依存しない処理。
  • パフォーマンスを優先する場合
    描画後に処理を実行することで、ユーザー体験を向上させます。

useLayoutEffectを選ぶべき場合

  • DOM操作が必要な場合
    DOM要素のサイズや位置を計算し、それを基に描画内容を調整する場合。
  • レイアウトのフラッシュを防ぐ場合
    描画前に変更を行うことで、視覚的な不具合を防ぐ必要がある場合。

コード例による違いの比較

以下の例では、両者の実行タイミングの違いを比較します。

import React, { useEffect, useLayoutEffect, useState } from 'react';

function TimingExample() {
  const [color, setColor] = useState('red');

  useEffect(() => {
    console.log('useEffect実行');
  });

  useLayoutEffect(() => {
    console.log('useLayoutEffect実行');
    setColor('blue'); // レンダリング前に色を変更
  });

  return (
    <div style={{ backgroundColor: color, height: '100px', width: '100px' }}>
      色変更のテスト
    </div>
  );
}

コンソールの出力順序

  1. useLayoutEffect実行
  2. 画面描画
  3. useEffect実行

この順序から分かるように、useLayoutEffectは描画前に実行され、useEffectは描画後に実行されます。

性能への影響


useLayoutEffectは同期的に実行されるため、処理が重い場合は描画が遅延し、アプリ全体のパフォーマンスに悪影響を与える可能性があります。一方、useEffectは非同期で動作するため、描画プロセスに影響を与えません。

選択の指針

  • DOMの操作やレイアウト調整が必要な場合: useLayoutEffect
  • それ以外の副作用処理: useEffect

これらの違いを理解することで、適切なフックを選択し、効率的なReactコンポーネントを構築できます。

useEffectの適切な使用タイミング

一般的な使用ケース


useEffectは、Reactコンポーネントのレンダリング後に非同期的に副作用を処理するため、以下のような場面で使用するのが適切です。

1. データのフェッチ


APIリクエストを送信し、取得したデータを状態として保存する場合に有効です。

useEffect(() => {
  async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    setData(data);
  }
  fetchData();
}, []); // 依存配列が空なので、初回レンダリング時のみ実行

2. イベントリスナーの登録


ウィンドウサイズ変更イベントなど、グローバルなイベントリスナーを登録する場合に使用します。必ずクリーンアップ処理を行い、リソースリークを防ぎます。

useEffect(() => {
  const handleResize = () => console.log('ウィンドウサイズが変更されました');
  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize); // クリーンアップ
  };
}, []);

3. サブスクリプションの設定


WebSocketやリアルタイムデータストリームなど、外部データとの接続を行う場合に適しています。

useEffect(() => {
  const subscription = someService.subscribe((data) => {
    console.log(data);
  });

  return () => {
    subscription.unsubscribe(); // クリーンアップ
  };
}, []);

依存配列による制御


useEffectは、依存配列を使って実行タイミングを制御します。適切に設定することで、不要な再実行を防ぎ、パフォーマンスを最適化できます。

空の依存配列 `[]`


初回レンダリング時のみ実行され、以降は実行されません。

特定の値を依存配列に含める `[dependency]`


指定した値が変更されたときにのみ実行されます。

useEffect(() => {
  console.log(`カウント: ${count}`);
}, [count]); // countが変更されたときに再実行

依存配列なし


コンポーネントがレンダリングされるたびに実行されます。ただし、パフォーマンスに悪影響を及ぼす可能性があるため、通常は避けるべきです。

useEffectの注意点

1. 不要な再実行の防止


依存配列を正確に設定することが重要です。不完全な依存配列は、予期しない動作や無限ループを引き起こす可能性があります。

2. クリーンアップの実装


イベントリスナーやタイマーの設定を行った場合は、必ずクリーンアップ関数を実装してリソースリークを防止します。

実用例: クリックカウントアプリ

以下の例では、ボタンがクリックされるたびにクリック回数を保存します。

import React, { useEffect, useState } from 'react';

function ClickCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`クリック回数: ${count}`);
  }, [count]); // countが更新されるたびに実行

  return (
    <button onClick={() => setCount(count + 1)}>
      クリック回数: {count}
    </button>
  );
}

useEffectは、Reactコンポーネントの非同期処理や副作用を管理する上で不可欠なフックです。適切に使用することで、パフォーマンスを最適化し、バグを防ぐことができます。

useLayoutEffectの適切な使用タイミング

useLayoutEffectを使うべきケース


useLayoutEffectは、DOMが更新された後、ブラウザが描画を行う前に実行されます。そのため、以下のようなケースで有効です。

1. DOMの測定とスタイル調整


DOM要素のサイズや位置を計測し、それを基にレイアウトを調整する場合に適しています。描画前に調整が行われるため、レイアウトの「ちらつき」を防止できます。

import React, { useLayoutEffect, useRef, useState } from 'react';

function LayoutExample() {
  const boxRef = useRef(null);
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    if (boxRef.current) {
      setWidth(boxRef.current.offsetWidth);
    }
  }, []); // 初回レンダリング時のみ実行

  return (
    <div>
      <div ref={boxRef} style={{ width: '50%', background: 'lightblue', height: '100px' }}>
        このボックスの幅: {width}px
      </div>
    </div>
  );
}

2. レイアウトの整合性確保


例えば、異なる要素間で高さを揃える場合や、動的に生成された要素の配置を調整する場合に役立ちます。

3. 描画の視覚的不具合を防ぐ


描画後の調整がユーザーに一瞬でも見えてしまう「フラッシュ」の問題を解決できます。これにより、ユーザーエクスペリエンスが向上します。

useEffectではなくuseLayoutEffectを選ぶ理由


通常、非同期で実行されるuseEffectでも副作用処理は可能ですが、以下の場合はuseLayoutEffectを選択すべきです。

  • DOM操作がユーザーの目に見える前に完了する必要がある場合
  • レイアウトの計算結果が次の描画に直接影響を与える場合

注意点

1. パフォーマンスへの影響


useLayoutEffectは同期的に実行されるため、処理が重い場合は描画の遅延を引き起こします。必ず必要最小限の操作に留めてください。

2. 不要な使用を避ける


多くの場合、useEffectで十分対応可能です。不要にuseLayoutEffectを使用することで、コードが複雑になり、パフォーマンスが低下する可能性があります。

実用例: モーダルのスクロール防止

以下の例では、モーダル表示中にページ全体のスクロールを防ぐ処理を行います。

import React, { useLayoutEffect } from 'react';

function Modal({ isOpen, onClose }) {
  useLayoutEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = '';
    }

    return () => {
      document.body.style.overflow = ''; // クリーンアップ処理
    };
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.5)' }}>
      <div style={{ backgroundColor: 'white', padding: '20px' }}>
        <h1>モーダルの内容</h1>
        <button onClick={onClose}>閉じる</button>
      </div>
    </div>
  );
}

選択の指針

  • DOMのスタイルやレイアウト調整が必要: useLayoutEffect
  • 非同期処理や画面描画後の操作: useEffect

適切なタイミングでuseLayoutEffectを使用することで、描画に関わるバグやパフォーマンスの問題を防ぐことができます。

よくある間違いとトラブルシューティング

初心者が陥りやすい間違い

1. 無駄な再実行を引き起こす


依存配列の指定ミスによって、useEffectやuseLayoutEffectが不要に再実行され、パフォーマンスが低下することがあります。

問題例:

useEffect(() => {
  console.log('不必要な再実行が発生しています');
}, [someState]); // 本来依存する必要のない変数を含めている

解決策:
依存配列には、本当に再実行が必要な値のみを正確に指定してください。静的な値には依存しないようにしましょう。


2. クリーンアップ処理を忘れる


イベントリスナーやタイマーを設定した場合、クリーンアップを忘れるとリソースリークや意図しない挙動を引き起こします。

問題例:

useEffect(() => {
  window.addEventListener('resize', () => {
    console.log('ウィンドウサイズ変更');
  });
  // クリーンアップ関数がない
}, []);

解決策:
クリーンアップ処理を必ず記述しましょう。クリーンアップ処理はuseEffect内で関数を返す形で実装します。

useEffect(() => {
  const handleResize = () => console.log('ウィンドウサイズ変更');
  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize); // クリーンアップ
  };
}, []);

3. useEffectとuseLayoutEffectの混同


DOMのスタイルやサイズを変更する目的でuseEffectを使用し、視覚的なちらつきを引き起こしてしまうケースがあります。

問題例:

useEffect(() => {
  document.body.style.backgroundColor = 'lightblue';
}, []);

解決策:
描画に影響を与える操作はuseLayoutEffectで行いましょう。これにより、ブラウザが描画する前に変更が適用されます。


トラブルシューティングの方法

1. 副作用の発生タイミングを確認


useEffectやuseLayoutEffectの中でログを出力し、実行タイミングを明確に把握します。

useEffect(() => {
  console.log('useEffectが実行されました');
}, []);

useLayoutEffect(() => {
  console.log('useLayoutEffectが実行されました');
}, []);

2. 依存配列の不備を修正


eslint-plugin-react-hooksを導入し、依存配列の指定が正しいかを静的解析で確認します。このプラグインは、useEffectの依存配列に必要な値を警告してくれるため、ミスを防ぐことができます。

npm install eslint-plugin-react-hooks --save-dev

3. パフォーマンスの問題を検出


不要な再レンダリングやフックの再実行が発生している場合、ReactのReact DevToolsを使用してコンポーネントのレンダリングプロセスをデバッグします。


チェックリストで確認

  1. 依存配列に正しい値を指定しているか?
  2. 必要に応じてクリーンアップ処理を実装しているか?
  3. useEffectとuseLayoutEffectの目的に応じた使い分けができているか?
  4. パフォーマンスに悪影響を与える処理をuseLayoutEffectで実行していないか?

これらのポイントを理解し、トラブルを防ぐことで、より安定したReactアプリケーションを構築することが可能になります。

演習問題:適切なフックを選択しよう

Reactの開発では、useEffectuseLayoutEffectを適切に選択することが重要です。以下の問題を通じて、どちらを使うべきかを判断する練習をしましょう。


問題1: データのフェッチ


APIからデータを取得し、コンポーネントに表示します。この場合に使用すべきフックはどちらでしょうか?

条件:

  • API呼び出しは非同期で行われます。
  • 描画後にデータを反映する必要があります。

解答例:
使用すべきフック: useEffect
理由: 描画後に実行されるため、非同期処理に適しており、UIスレッドをブロックしません。


問題2: DOMのサイズを計測


コンポーネント内のDOM要素の幅を計測し、その値を状態に反映します。この場合に使用すべきフックはどちらでしょうか?

条件:

  • DOMのサイズを正確に取得し、それを基に描画を調整する必要があります。
  • ユーザーに描画の「ちらつき」を見せたくありません。

解答例:
使用すべきフック: useLayoutEffect
理由: 描画前にDOM操作を行うことで、レイアウトのちらつきを防ぎます。


問題3: ウィンドウリサイズイベントの登録


ウィンドウサイズ変更イベントを監視し、サイズを状態として管理します。この場合に使用すべきフックはどちらでしょうか?

条件:

  • 描画後にイベントリスナーを登録します。
  • サイズ変更イベントに基づいて状態を更新します。

解答例:
使用すべきフック: useEffect
理由: DOMの操作は必要なく、描画後にイベントリスナーを登録するだけで十分です。


問題4: 動的スタイルの設定


ボタンがクリックされたときに、親要素の背景色を変更します。この場合に使用すべきフックはどちらでしょうか?

条件:

  • 背景色の変更はユーザー操作に基づき行います。
  • 描画後に背景色が変更されても問題ありません。

解答例:
使用すべきフック: useEffect
理由: DOMの操作は描画後で問題ないため、非同期的なuseEffectが適しています。


実践問題: コンポーネントの改善


以下のコードでは、フックの選択に誤りがあります。正しいフックを選び、コードを改善してください。

コード例:

import React, { useLayoutEffect, useState } from 'react';

function FetchComponent() {
  const [data, setData] = useState(null);

  useLayoutEffect(() => {
    async function fetchData() {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();
      setData(result);
    }
    fetchData();
  }, []);

  return <div>{data ? JSON.stringify(data) : 'ロード中...'}</div>;
}

問題点:

  • useLayoutEffectを使用していますが、APIフェッチに描画前の処理は必要ありません。

改善後のコード:

import React, { useEffect, useState } from 'react';

function FetchComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();
      setData(result);
    }
    fetchData();
  }, []);

  return <div>{data ? JSON.stringify(data) : 'ロード中...'}</div>;
}

解説:
非同期処理にはuseEffectを使用することで、描画に影響を与えず効率的なコードになります。


これらの演習問題を通じて、useEffectuseLayoutEffectを適切に使い分けるスキルを磨きましょう。実践的な経験を積むことで、React開発での生産性とコード品質が向上します。

まとめ

本記事では、ReactのuseEffectuseLayoutEffectの違いと適切な使い分けについて詳しく解説しました。useEffectは非同期処理や副作用を管理するための基本的なフックであり、主に描画後に実行される処理に適しています。一方、useLayoutEffectは描画前に同期的に実行されるため、DOM操作やレイアウトの調整が必要な場面で有効です。

両者を適切に選択することで、アプリケーションのパフォーマンスを向上させ、予期しないバグを防ぐことができます。記事で紹介した演習問題や例を通じて、フックの正しい使い方を習得し、React開発に役立ててください。

コメント

コメントする

目次