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} />