useEffectに配列値を指定している場合に、意図せず再評価されるケースが合ったのでメモ
useEffectのdependenciesと配列値
以下のようなコードで、setValueで値が変更されて子コンポーネントが再評価されるとき、デフォルト引数の配列も再度生成されて参照が変わるため useEffect()
が再度評価される
function Hoge({ items = [] }: { items?: number[] }) {
useEffect(() => {
console.log(items);
}, [items]);
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
}
export default function App() {
return (
<Hoge />
)
}
呼び出し元で固定の配列を指定すると子コンポーネントの再評価時でitemsの参照が変わらない。
export default function App() {
return (
<Hoge items={[]}/>
)
}
ただ、親コンポーネントが再評価されると、毎回新しい配列が生成されて参照が変わってしまう。
export default function App() {
const [value, setValue] = useState('');
return (
<>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Hoge items={[]} />
</>
)
}
なので関数の外側でconst定義したものを固定配列として渡すと参照が変わらないのでuseEffectが再度実行されない。
const defaultItems: number[] = [];
export default function App() {
return (
<Hoge items={defaultItems}/>
)
}
ちなみにプリミティブは大丈夫
function Fuga({ number = 1, string = '2', boolean = true }: { number: number, string: string, boolean: boolean }) {
useEffect(() => {
console.log(number, string, boolean);
}, [number, string, boolean]);
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
}
ということでこの問題は配列だけではなくオブジェクトや関数でも発生する。 constや外部関数などで参照を固定化しておくか、useMemoやuseCallbackなどでメモ化すれば良い。ただし、useMemo, useCallbackに関しては依存配列が結局その配列・オブジェクト・関数になりがちなので、結局useEffect()の依存配列に入れない、という選択肢になりそう。
そもそも useEffect()
で配列を使いたいケースというのは大抵は一覧検索系のAPIリクエストで複数値でフィルタリングしたいケースだと思われる。条件を渡せばAPIリクエストしつつその結果をレンダリングする一覧コンポーネントのようなケースがわかりやすい。ただ、このケースは条件自体が useState()
で状態管理されていることがほとんどで、 useState()
の値はstableなのでこの問題は発生しない。ということであまり発生しない気もするが、 useEffect()
のdependenciesの値がstableかどうかには気をつけていきたいし、そもそも そのエフェクトは不要かも しれない。
オブジェクトの参照が同じかどうかを判別する
以下のような関数を外部に定義しておき、 getRefId(obj)
の値をデバッグすると参照が変わっているかどうかがわかる。
const map = new WeakMap();
let refId = 0;
function getRefId(obj: object) {
if (map.has(obj)) {
return map.get(obj);
}
const id = refId++;
map.set(obj, id);
return id;
}