useEffectの第二引数には何を指定すればよいのか
useEffectの第二引数として配列を渡すことで配列に指定した値それぞれに変更があった場合のみuseEffect内の関数が実行されます。
useEffect内でなにか関数を実行する際、第二引数として関数も渡すことがあると思いますが、これが正しいのかどうか分からず調べてみました。
useStateのset関数は第二引数に必要?
例えばAPIから取得したデータをstateにセットするみたいなコードがあったとします(コードは適当です)
const Component = () => {
const [count, setCount] = useState(0);
const { response } = useAPI();
useEffect(() => {
setCount(response.length);
}, [response, setCount]); // ここでsetCountって必要?
}
このset関数はuseEffectの第二引数に含める必要があるのでしょうか?結論から言うと上記のコードでは setCount
はセットする必要はありません。
useStateのドキュメントに以下のような記述があり、再レンダリングされた場合もset関数は同一性が担保されておりuseEffectの依存リスト(第二引数)に含めても値が変わることがないので入れる必要がありません。
useReducerも同じような記述がありdispatchも入れる必要がありません。
通常の関数は再レンダリング時に同一性がない
以下のように書くと、関数で実行する内容は変わらないにもかかわらず再レンダリングされるたびにuseEffectが実行されます。
const Component = () => {
const [count, setCount] = useState(0);
const hello = () => {
console.log('hello!');
}
useEffect(() => {
hello();
}, [hello]);
}
これは再レンダリングのたびに関数が再生成されるため、変更があったかどうかを判断するObject.is()はfalseとなりuseEffectが実行されます。↓のイメージです。
Object.is(() => null, () => null); // => false
上記の場合どうするのがいいのかというと、A Complete Guide to useEffectによれば、propsやstateに依存しない関数の場合はコンポーネントの外で関数を定義し、依存する場合はuseEffect内で関数を定義するのがよいとのこと。
// コンポーネントの外で定義
const hello = () => {
console.log('hello!');
}
const Component = () => {
const [count, setCount] = useState(0);
useEffect(() => {
hello();
}, []);
}
const Component = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// useEffect内で定義
const hello = () => {
console.log(data);
}
hello();
}, [data]);
}
useCallbackを使って関数自体をメモ化して再レンダリングされた場合でも同一のものを返すということもできますが、公式ドキュメントには最終手段と書いているので、上記2つで書くことができない場合に使うとよさそうです。
const Component = () => {
const [count, setCount] = useState(0);
const hello = useCallback() => {
console.log(data);
}, [])
useEffect(() => {
hello();
}, [hello]);
}
ESLintのeslint-plugin-react-hooksプラグイン
ESLintのeslint-plugin-react-hooksプラグインを利用しreact-hooks/exhaustive-depsのルールをONにすることで、useEffect, useCallbackの第二引数を自動でチェックすることができます
上記のuseStateのset関数やuseReducerのdispatch関数、useRefのrefはあらかじめ除外されており(ソースコード)、チェックされないので含める必要がありません。また、通常の関数についても再レンダリングのたびにuseEffectが実行される場合はlintで教えてくれます。
カスタムフックに処理を出している場合に注意が必要
カスタムフック内で関数を定義し、それをコンポーネント側で利用するときはこのようなコードになるかと思います。
const useHello = () => {
const hello = (count) => {
console.log('hello! count', count);
}
return { hello };
}
const Component = () => {
const [count, setCount] = useState(0);
const { hello } = useHello();
useEffect(() => {
hello(count);
}, [count, hello]);
}
このコンポーネントが再レンダリングされたとき、countが変更されているいないに関わらずuseEffectが実行されます。この場合はESLintのreact-hooks/exhaustive-depsでも検知することはできないため、カスタムフックの実装時に関数は同一性が担保されるように実装する必要があります。
関数の実行結果を第二引数に指定してもよいか
あまり書かないと思いますがこんなコードです。
const neededCountUp = () => {
return true;
};
const Component = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prev => prev + 1);
}, [neededCountUp()]);
}
これも結論から言うと動作はしますが、当然再レンダリング時に前回の情報とのObject.is()で変更があったかを判断するため、上記のコードでは関数の返り値として常にtrueが返ってきており2回目以降のレンダリング時にはuseEffectは実行されしません。再レンダリング時に関数が毎回実行され返り値からuseEffectを実行させるか判断するという挙動となりあまり効率は良くないコードかもしれません。
ちなみに上記のようなコードではESLintoのreact-hooks/exhaustive-depsではチェックにかかり、関数実行の返り値の別の変数にいれましょうというメッセージが出ます。
Discussion