Reactの開発において、useEffectとuseLayoutEffectは、状態管理や副作用の処理を行う上で欠かせないフックです。しかし、それぞれがどのように異なり、どのような場面で使い分けるべきかについて、初心者には難しいと感じるかもしれません。本記事では、両者の基本的な概要と違いから、具体的な使用例、適切な使い分け方、そしてよくある間違いまで、詳しく解説します。この内容を学ぶことで、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>
);
}
コンソールの出力順序
- useLayoutEffect実行
- 画面描画
- 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を使用してコンポーネントのレンダリングプロセスをデバッグします。
チェックリストで確認
- 依存配列に正しい値を指定しているか?
- 必要に応じてクリーンアップ処理を実装しているか?
- useEffectとuseLayoutEffectの目的に応じた使い分けができているか?
- パフォーマンスに悪影響を与える処理をuseLayoutEffectで実行していないか?
これらのポイントを理解し、トラブルを防ぐことで、より安定したReactアプリケーションを構築することが可能になります。
演習問題:適切なフックを選択しよう
Reactの開発では、useEffectとuseLayoutEffectを適切に選択することが重要です。以下の問題を通じて、どちらを使うべきかを判断する練習をしましょう。
問題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を使用することで、描画に影響を与えず効率的なコードになります。
これらの演習問題を通じて、useEffectとuseLayoutEffectを適切に使い分けるスキルを磨きましょう。実践的な経験を積むことで、React開発での生産性とコード品質が向上します。
まとめ
本記事では、ReactのuseEffectとuseLayoutEffectの違いと適切な使い分けについて詳しく解説しました。useEffectは非同期処理や副作用を管理するための基本的なフックであり、主に描画後に実行される処理に適しています。一方、useLayoutEffectは描画前に同期的に実行されるため、DOM操作やレイアウトの調整が必要な場面で有効です。
両者を適切に選択することで、アプリケーションのパフォーマンスを向上させ、予期しないバグを防ぐことができます。記事で紹介した演習問題や例を通じて、フックの正しい使い方を習得し、React開発に役立ててください。
コメント