はじめに
useEffect
の依存配列の役割について、しっかりと理解できていますか?
私はなんとなくで実装をしていたため、業務で困った事態に直面することとなりました。
この記事は依存配列によって挙動がどのように変わるかをまとめ、useEffect
への理解を深めるものです。
環境
本記事の実装においては、以下のものを使っています。
ライブラリ | バージョン |
---|---|
React | 18.2.0 |
TypeScript | 5.2.2 |
Vite | 5.2.0 |
useEffectとは
useEffect
の基本的な作りは
useEffect(実行したい処理, [依存値])
となります。
第2引数の依存値の配列は省略可能です。
コンポーネントの読み込み時、および依存値の配列が更新されたときに実行したい処理が実行されます。
useEffect
の本質的な使い方はここでは割愛します。
依存値の配列(依存配列)の設定方法によって処理の実行タイミングが異なるので、3つの設定パターンを確認します。
処理の前提となるコード
3つのコードを用意しました。
冗長になるので、コード部分は折りたたんでいます。
App.tsx
のuseEffect
の処理をそれぞれのパターンごとに変えて、その挙動の違いを確認します。
前提となるコードを確認する
import { useEffect, useState } from "react";
import Display from "./Display";
import Register from "./Register";
const App = () => {
// カウント用
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
const decrementCount = () => setCount(count - 1);
// ラジオボタン用
const [direction, setDirection] = useState("");
console.log("component loaded");
// 処理に合わせて編集
useEffect();
return (
<>
<Display count={count} direction={direction} />
<Register
increment={incrementCount}
decrement={decrementCount}
direction={direction}
setDirection={setDirection}
/>
</>
);
};
export default App;
// propsの型
interface DisplayProps {
count: number;
direction: string;
}
const Display = ({ count, direction }: DisplayProps) => {
return (
<>
<div>Count:{count}</div>
<div>Direction:{direction}</div>
</>
);
};
export default Display;
import React from "react";
import { ChangeEvent } from "react";
// ラジオボタン定数
const VALUES = ["left", "right"];
// propsの型
interface RegisterProps {
increment: () => void;
decrement: () => void;
direction: string;
setDirection: (direction: string) => void;
}
const Register = ({
increment,
decrement,
direction,
setDirection,
}: RegisterProps) => {
// ラジオボタン変更処理
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDirection(event.currentTarget.value);
};
return (
<>
<div>
<button onClick={increment}>count up</button>
<button onClick={decrement}>count down</button>
</div>
<div>
{VALUES.map((value) => {
return (
<React.Fragment key={value}>
<label htmlFor={value}>{value}</label>
<input
type="radio"
name="direction"
id={value}
value={value}
checked={value === direction}
onChange={handleChange}
/>
</React.Fragment>
);
})}
</div>
</>
);
};
export default Register;
中身のある依存配列を設定しているパターン
// 依存配列に「count」を設定
useEffect(() => {
console.log("execute useEffect");
}, [count]);
依存配列にcount
を設定しています。
よってuseEffect
内に定義した処理は
- 初めの画面読み込み時
-
count
の変更時
に実行されます。
初期読み込み
「component loaded」はコンポーネントがレンダリングされるたびに出力するように設定しているログです。
比較のために用意しました。
「execute useEffect」はuseEffect
の処理が実行されると出力されるログです。
初めの画面読み込み時なのでこれら2つのログが出力されます。
countの変更
「count up」ボタン、または「count down」ボタンを押すことでcount
の値を変更できます。
まず、「component loaded」が出力されます。
(count
はApp.tsx
で定義されているstate
で、state
が更新されるとコンポーネントは再レンダリングされるため。)
そして、依存配列に指定したcount
が変更されたため、useEffect
の処理も実行され、「execute useEffect」が出力されます。
これは想定通りの挙動です。
directionの変更
ラジオボタンの値を変更することで、direction
を変更できます。
direction
もstate
なのでコンポーネントが再レンダリングされ、「component loaded」が出力されます。
しかし、「execute useEffect」は出力されていません。
コンポーネントが再レンダリングされたとしても、direction
は依存配列には関係の無い値なのでuseEffect
は出力されません。
空の依存配列を設定しているパターン
// 依存配列に空の配列を設定
useEffect(() => {
console.log("execute useEffect");
}, []);
中身のない配列を設定した場合はどうなるでしょうか。
依存する値がないため、useEffect
の実行のためのトリガーは
- 初めの画面読み込み時
のみになります。
初期読み込み
挙動は中身のある依存配列と変わらないので省略します。
countの変更
※初めの2行は初期読み込み時の出力です。
count
の変更によりコンポーネントは再レンダリングされますが、useEffect
は実行されていません。
directionの変更
こちらもdirection
の変更によりコンポーネントが再レンダリングされますが、useEffect
は実行されません。
つまり、依存配列を空にした場合はコンポーネントの初期読み込み時にのみ実行される処理を定義できる、ということになります。
依存配列を省略しているパターン
// 依存配列を省略
useEffect(() => {
console.log("execute useEffect");
});
依存配列を省略した場合、空の配列のときと同じで依存する値がないので、初期読み込み時の実行になりそうです。
初期読み込み
挙動は中身のある依存配列と変わらないので省略します。
countの変更
※初めの2行は初期読み込み時の出力です。
空の配列のときとは異なり、useEffect
が実行されていることがわかります。
directionの変更
こちらもuseEffect
が実行されていることがわかります。
つまり、配列を省略した場合には、コンポーネントが再レンダリングされるたび実行されることになります。
空の配列のときも、配列を省略したときも、どちらも同じような感じがしますがまったく逆の挙動になりました。
まとめ
依存配列の設定方法について3パターン確認しました。
空の依存配列を設定しているパターンと、依存配列を省略しているパターンを同じと考えていたため、想定外の挙動に悩まされていました。
また、useEffect
の役割そのものも誤解していたので、ここに関しては別途改めて記事を書いて整理したいと思います。