【入門編】 eslint-plugin-react-hooksの依存配列ルール – React実践ガイド

Reactの「useEffect」、依存配列の罠と賢い付き合い方〜eslint-plugin-react-hooksのexhaustive-depsルールが教えてくれること〜

ようこそ、Reactの世界へ!✨

フロントエンドの世界に足を踏み入れたばかりの皆さん、そしてReactの「useEffect」を使い始めたばかりの皆さん、きっと「これ、どういう時に動くんだっけ?」とか、「なんで思った通りに動かないんだろう?」って、ちょっと戸惑う瞬間があるんじゃないかな?

特に「useEffect」の第2引数にある「依存配列」、あれ、最初は「なんだかよくわからないけど、とりあえず空っぽにしとけばいいのかな?」とか、「なんかエラーが出るけど、無視しちゃえ!」なんて、ちょっとした「おまじない」みたいに捉えてしまいがち。でもね、実はこの依存配列、Reactのパフォーマンスや意図しないバグの温床になりやすい、とってもデリケートな部分なんです。

今日は、そんな「useEffect」の依存配列を味方につけるための、魔法の杖みたいな存在、「eslint-plugin-react-hooks」の「exhaustive-deps」ルールについて、一緒にゆるーく、でもしっかり学んでいきましょう!まるで、お気に入りの道具を使いこなすように、Reactのコードをスッキリ、そして安全に書けるようになるはずですよ。

「useEffect」って、そもそも何者?〜お家の「換気扇」みたいなもの〜

まず、基本中の基本からおさらいです。「useEffect」って、一体何をしてくれるんでしょう?

皆さんがお家で料理をすると、どうしても油や匂いが充満しますよね?そんな時、私たちは「換気扇」をつけます。換気扇は、料理が終わった後や、特定のタイミングで「空気を入れ替える」という役割を果たしてくれます。

Reactの「useEffect」も、これと似たような役割を担っているんです。コンポーネントが表示された時、更新された時、あるいは消える時といった、「特定のタイミング」で「何か処理を実行したい」時に使うのが「useEffect」です。

例えば、

  • コンポーネントが表示されたら、APIからデータを取ってくる
  • ユーザーが何か入力したら、その内容をローカルストレージに保存する
  • タイマーを設定して、一定時間後に何かを実行する

…なんていう、コンポーネントの外側にあるもの(副作用)とやり取りしたい時に、大活躍してくれるんです。

import React, { useState, useEffect } from ‘react’;

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

// コンポーネントが表示された時(初回)に、コンソールにメッセージを表示する
useEffect(() => {
console.log(‘Component is mounted!’); // コンポーネントがマウントされました!
// ここに、API通信などの副作用処理を書くことが多い
}, []); // ← この [] が「依存配列」の始まり!

return (

Count: {count}

);
}

export default ExampleComponent;

この `useEffect` の第2引数にある `[]`、これが「依存配列」です。この配列に書かれたものが変化した時に、`useEffect` の中の処理(コールバック関数)が実行される、という仕組みになっています。

依存配列の「空っぽ」と「何もない」は、全然違う!〜「毎回」動かしたい?「一度だけ」?〜

さて、ここからが本題の「依存配列」のお話です。

1. 依存配列が「空っぽ」 `[]` の場合

先ほどの例のように、依存配列が `[]`(空っぽ)の場合、これは「この `useEffect` は、コンポーネントが最初に画面に表示された(マウントされた)時、一度だけ実行してくださいね」という意味になります。

まさに、お家を建てて、最初に「換気扇を一度だけ点検する」みたいなイメージです。一度点検したら、その後は基本的に触る必要はありませんよね?

2. 依存配列が「何もない」場合(省略した場合)

では、依存配列を 省略 した場合、どうなるでしょうか?

import React, { useState, useEffect } from ‘react’;

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

// 依存配列を省略した場合
useEffect(() => {
console.log(‘Count has changed!’); // カウントが変わりました!
// この処理は、countが変わるたびに実行される
}); // ← 依存配列を省略!

return (

Count: {count}

);
}

export default ExampleComponent;

この場合、`useEffect` の中の処理は、コンポーネントが 「更新されるたびに」 実行されます。

これは、まるで「料理をするたびに、毎回換気扇を自動でつけて、空気を入れ替える」ようなイメージです。料理をするたびに(`count` が変わるたびに)、換気扇が動く(`console.log` が実行される)わけですね。

「exhaustive-deps」ルールって、一体何をしてくれるの?〜頼れる「お買い物リスト」〜

さて、ここで登場するのが、我らがヒーロー、「eslint-plugin-react-hooks」の「exhaustive-deps」ルールです!

このルールは、Reactの公式が推奨している「React HooksのためのESLintプラグイン」に含まれている、とっても賢いルールの一つです。

皆さんがお買い物に行く時、欲しいものをリストアップしておくと、買い忘れがなくて効率的ですよね?「exhaustive-deps」ルールも、それに似たことをしてくれます。

「useEffect」の中で使っている変数は、ちゃんと依存配列に入っていますか?

と、常にチェックしてくれるんです。

もし、`useEffect` の中で使っている変数(例えば、さっきの `count`)が、依存配列に入っていないと、「あれ?この `count`、もしかしたら使いたいんじゃない?なのに、依存配列に入ってないよ?もしかして、バグの原因になるかも!」と、優しく(でも、ちょっと厳しめに)警告してくれるんですね。

これが、いわゆる「依存配列が不十分だよ!」という警告です。

依存配列の「不備」が引き起こす、こわーい話〜「あれ?なんか想定外の動きをしてるぞ?」〜

なぜ、この「exhaustive-deps」ルールがそこまで重要なのでしょうか?それは、依存配列の不備が、意図しないバグやパフォーマンスの低下を招くことがあるからです。

1. 意図しない無限ループの発生

一番怖いのは、無限ループです。

例えば、こんなコードを考えてみてください。

import React, { useState, useEffect } from ‘react’;

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

// 依存配列に count を入れ忘れた場合
useEffect(() => {
// count が 10 未満だったら、count を1増やす
if (count < 10) { setCount(count + 1); // ここで setCount を呼ぶ } console.log('Effect ran, count is:', count); }); // ← 依存配列を省略(または [] にしてしまうと…) return (

Count: {count}

);
}

export default InfiniteLoopComponent;

このコード、一見すると「count が10になるまで増える」ように見えますよね?
でも、依存配列を省略(または `[]` に)してしまうと、`useEffect` の中の `setCount(count + 1)` が実行されるたびに、コンポーネントが再レンダリングされます。そして、再レンダリングされるたびに `useEffect` がまた実行されて…

…あれ?

「count が 10 未満だったら、count を1増やす」
↓
「count が増えたから、コンポーネントが再レンダリング」
↓
「再レンダリングされたから、useEffect がもう一度実行」
↓
「count が 10 未満だったら、count を1増やす」
↓
…無限ループ!

ブラウザが固まってしまう、あの恐怖の無限ループに陥ってしまうんです!

`exhaustive-deps` ルールは、このような「useEffect の中で使っている変数が、依存配列に入っていない」という状態を検知し、警告してくれるので、この悪夢を防いでくれるんですね。

2. 古い情報を使った処理

もう一つは、古い情報を使った処理になってしまうケースです。

例えば、ある関数を `useEffect` の中で呼び出すとします。

import React, { useState, useEffect } from ‘react’;

function OldDataComponent() {
const [text, setText] = useState(”);
const [message, setMessage] = useState(”);

// 外部で定義された、ちょっと古い情報を使うかもしれない関数
const generateMessage = (inputText) => {
return `Your input was: ${inputText} (processed at ${new Date().toLocaleTimeString()})`;
};

// 依存配列に generateMessage を入れ忘れた場合
useEffect(() => {
const newMessage = generateMessage(text); // text を使っている
setMessage(newMessage);
}); // ← generateMessage も text も、依存配列に入っていない…

return (

setText(e.target.value)}
placeholder=”Enter text”
/>

Message: {message}

);
}

export default OldDataComponent;

この `generateMessage` 関数は、受け取った `inputText` を元にメッセージを生成し、さらにその時の現在時刻も表示するとします。

ここで、もし `useEffect` の依存配列に `text` や `generateMessage` を入れ忘れてしまうと、どうなるでしょう?

`text` が更新されても `useEffect` が再実行されないので、`message` は古いまま。
あるいは、もし `generateMessage` 関数がコンポーネントの外で定義されていたり、props で渡されたりして、その関数自体が更新されたとしても、`useEffect` はそれを検知しない、なんてことも起こりえます。

結果として、ユーザーは最新の入力内容 (`text`) を反映したメッセージ (`message`) を見ることができず、「あれ?入力してるのに、全然変わらないぞ?」という、がっかりな体験をしてしまうのです。

「exhaustive-deps」ルールは、このような「`useEffect` の中で使っているけれど、依存配列に入っていない変数」を教えてくれるので、「常に最新の情報を使って処理を実行する」という、当たり前だけど大切なことを実現してくれるんです。

「 useCallback」と「useMemo」〜賢く依存関係を安定させるテクニック〜

「でも、何でもかんでも依存配列に入れると、逆にパフォーマンスが悪くなるんじゃない?」
「関数を依存配列に入れると、毎回新しい関数が作られちゃって、無限ループになっちゃうかも…」

そう思ったあなた、鋭い!

確かに、毎回新しい関数やオブジェクトが作られてしまうと、それが原因で `useEffect` が頻繁に実行されてしまい、パフォーマンスが悪化する可能性があります。

そこで登場するのが、「useCallback」と「useMemo」という、Reactのフックたちです。これらは、関数や値の「安定化」に役立ちます。

1. `useCallback` 〜関数を「固定」する〜

`useCallback` は、関数を「メモ化」します。つまり、「一度作った関数は、依存配列に変化がない限り、常に同じ関数を使い回してくださいね」という指示になります。

import React, { useState, useEffect, useCallback } from ‘react’;

function UseCallbackExample() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(false);

// useCallback を使って、 handleClick をメモ化する
const handleClick = useCallback(() => {
console.log(‘Button clicked!’);
setCount(count + 1); // count を使っているので、依存配列に count を含める
}, [count]); // ← count が変わらない限り、この handleClick は再生成されない

useEffect(() => {
console.log(‘Effect is running…’);
// ここで handleClick を呼び出すなど…
// handleClick(); // 依存配列に handleClick を入れないと警告が出る
}, [handleClick]); // ← handleClick を依存配列に入れる

return (

Count: {count}



{/ props で渡す場合などに、handleClick の安定化が役立つ /}

);
}

// 子コンポーネント(props で関数を受け取る例)
function SomeChildComponent({ onClick }) {
console.log(‘Child component rendered’);
return ;
}

export default UseCallbackExample;

この例では、`handleClick` という関数を `useCallback` で囲っています。依存配列に `count` を指定しているので、`count` が変わらない限り、`handleClick` 関数は再生成されません。

これにより、`useEffect` の依存配列に `handleClick` を入れても、`otherState` が変わってコンポーネントが再レンダリングされたとしても、`handleClick` が再生成されないため、`useEffect` は意図せず再実行されにくくなります。

`useCallback` は、特に子コンポーネントにコールバック関数を props として渡す場合に、子コンポーネントの不要な再レンダリングを防ぐために非常に有効です。

2. `useMemo` 〜値を「固定」する〜

`useMemo` は、計算結果のような「値」をメモ化します。

import React, { useState, useEffect, useMemo } from ‘react’;

function UseMemoExample() {
const [number, setNumber] = useState(10);
const [otherValue, setOtherValue] = useState(0);

// useMemo を使って、expensiveCalculation の結果をメモ化する
const calculatedValue = useMemo(() => {
console.log(‘Performing expensive calculation…’);
// 時間のかかる計算だと仮定
let result = 0;
for (let i = 0; i < number; i++) { result += i; } return result; }, [number]); // ← number が変わらない限り、この計算は実行されない useEffect(() => {
console.log(‘Effect is running…’);
// calculatedValue を使った処理
console.log(‘Calculated value:’, calculatedValue);
}, [calculatedValue]); // ← calculatedValue を依存配列に入れる

return (

Number: {number}

Other Value: {otherValue}

Expensive Calculation Result: {calculatedValue}

);
}

export default UseMemoExample;

この例では、`useMemo` を使って、`number` に依存した重い計算の結果をメモ化しています。`number` が変わらない限り、`useMemo` の中の計算処理は実行されず、以前計算された値がそのまま返されます。

これにより、`otherValue` が更新されてコンポーネントが再レンダリングされても、「expensive calculation」は実行されず、パフォーマンスの低下を防ぐことができます。

`useCallback` は関数を、`useMemo` は値を安定させると覚えておくと良いでしょう。これらを適切に使うことで、「exhaustive-deps」ルールが提案する依存配列の管理を、より効率的に行うことができます。

依存配列のルールを「無視」する際の、禁断の果実…

さて、ここまで「exhaustive-deps」ルールの重要性を熱く語ってきましたが、中には「どうしてもこの警告を消したいんだ!」という場面もあるかもしれません。

例えば、

  • クリーンアップ関数でしか使わない変数
  • グローバルな値で、そもそも変化しないことが保証されている変数
  • どうしても、この `useEffect` だけは、毎回実行させたい(ただし、無限ループには注意!)

…といった、特別な理由がある場合です。

そんな時は、ESLintのコメントを使って、このルールを一時的に無効化することができます。

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
// … ここにコードを書く …
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dependency1, dependency2]); // 明示的に依存配列を指定しつつ、特定の変数を無視する

ただし、これは「禁断の果実」だと思ってください!

このコメントを使うということは、「この警告は、私が理解した上で、意図的に無視していますよ」という、開発者としての強い宣言になります。

もし、このコメントを使った箇所でバグが発生した場合、それは「eslint-plugin-react-hooks」のせいにできません。あなた自身の責任で、そのバグの原因を特定し、修正する必要が出てきます。

だから、このコメントを使うのは、本当に最後の手段。そして、なぜ無視する必要があるのかを、コードのコメントなどで明確に残しておくことを強くおすすめします。

まとめ:Reactの「useEffect」と上手に付き合うために

長くなってしまいましたが、いかがでしたでしょうか?

「useEffect」の依存配列、そして「exhaustive-deps」ルールは、React開発において、知れば知るほど奥深く、そして強力な味方になってくれる存在です。

  • 「useEffect」は、コンポーネントの「副作用」を管理するためのもの。
  • 依存配列は、「いつ」useEffect の中の処理を実行するかを決める、とっても大事な設定。
  • `[]` は「初回だけ」、省略は「毎回」実行される。
  • 「exhaustive-deps」ルールは、依存配列の漏れや間違いを教えてくれる、頼れる番人。
  • 依存配列の不備は、無限ループや意図しないバグの原因になることがある。
  • 「useCallback」と「useMemo」は、関数や値を安定させ、パフォーマンスを最適化するのに役立つ。
  • 依存配列の無視は、最後の手段。使うときは、責任を持って。

まるで、お料理のレシピを正確に守るように、この依存配列のルールを守ることで、あなたのReactコードは、より安全で、より予測可能で、そしてより効率的なものになるはずです。

最初は少し戸惑うかもしれませんが、この「exhaustive-deps」ルールを意識してコードを書く練習を重ねるうちに、きっと「なるほど!」と腑に落ちる瞬間が訪れるはずです。

さあ、あなたも「exhaustive-deps」ルールを味方につけて、もっともっとReact開発を楽しんでいきましょう!応援しています!😊

コメント

タイトルとURLをコピーしました