React Hooks の useEffect において、よく間違えやすい注意点をここでは解説していきたいと思います。
useEffect の基礎的な内容を学びたいかたはこちらからご覧ください!
useEffect に渡すコールバック関数を Promise にしてはいけない
useEffect
内で非同期処理を行うために、第一引数のコールバック関数に Promise を渡してしまうことがあるかもしれません。例えば以下のような例が考えられます。
import React, { useEffect } from "react";
const UseEffectAnti1: React.FC<{ query: string }> = ({ query }) => {
useEffect(async () => {
const response = await fetch(
`https://www.googleapis.com/books/v1/volumes?q=${query}`
);
console.log(response);
}, [query]);
return <div>query: {query}</div>;
};
export default UseEffectAnti1;
上記の例では、props に API 検索用のクエリを渡しており、それが変更される度にリクエストを行うようになっています。
この問題点としては、非同期処理を行いたいがために、useEffect
の引数に非同期関数を渡してしまっている点です。そのせいでPromise 型が戻り値として設定されるため、エラーとなります。(useEffect 内のコールバック関数の戻り値はクリーンアップ関数を設定しないといけない)
解決策(1)
そのため、正しくは useEffect のコールバック関数の内部に非同期関数を定義し、呼び出すようにしてあげれば大丈夫です。以下のように修正しましょう。
import React, { useEffect } from "react";
const UseEffectAnti1: React.FC<{ query: string }> = ({ query }) => {
useEffect(() => {
async function fetchEmployees() {
const response = await fetch(
`https://www.googleapis.com/books/v1/volumes?q=${query}`
);
console.log(response);
}
fetchEmployees();
}, [query]);
return <div>query: {query}</div>;
};
export default UseEffectAnti1;
useEffect 内部で毎秒 state を更新することができない(ライフサイクルモデルとの混合)
以下は、毎秒 count が 1 ずつ増え、表示されると想定しているが、実際 count は 1 のままになってしまうといった例です。
import React, { useEffect, useState } from "react";
const UseEffectAnti2: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default UseEffectAnti2;
なぜuseEffect
のsetInterval
内部で count は更新されないのでしょうか。原因はuseEffect
の第二引数に[]
を指定しているためです。
関数コンポーネントは、レンダリングされる毎にそれぞれuseEffect
をもっており(props, state なども同様です)、特定のレンダリング時の state を参照することになります。
上記の例だと、初回のレンダリング時の count を参照することになり、count=0
であることがわかります。そして、useEffect
の第二引数に依存する値を入れていないので、それ以降 count が変更されてもuseEffect
のコールバック関数が実行されることはありません。
つまり、setCount(0 + 1)
を毎秒繰り返すことになります。
解決策(2)
依存する値を減らす
シンプルな解決策としては、第二引数の依存配列に必ず依存してる値をいれることです。今回の場合 count を入れます。
import React, { useEffect, useState } from "react";
const UseEffectAnti2: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <div>{count}</div>;
};
export default UseEffectAnti2;
以下は毎レンダリング時のイメージです。
// 初回レンダリング時
const UseEffectAnti2: React.FC = () => {
...
useEffect(() => {
const id = setInterval(() => {
//
setCount(0 + 1);
}, 1000);
return () => clearInterval(id);
}, [0]);
...
};
// 2回目のレンダリング時 count = 1に変化
const UseEffectAnti2: React.FC = () => {
// count =
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(1 + 1);
}, 1000);
return () => clearInterval(id);
}, [1]);
...
};
useEffect
が count の変化をキャプチャできていることがわかります。
lint rule の導入
eslint-plugin-react-hooks@next を導入することで依存している値を入れていない時エラーが表示されるため、事前に防ぐことが可能です。
setState の引数にコールバック関数を使う
前の state に基づいて state をアップデートしたい場合は、setState の関数型の更新を使うことで count を参照することがなくなり、依存配列に書く必要がなくなります。
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
useEffect が無限ループに陥ってしまう
次に、以下のようなコンポーネントを考えましょう。ぱっと見、問題なさそうなコードに見えます。
import React, { useEffect, useState } from "react";
const UseEffectAnti3: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
return (
<>
<h3>click count : {count}</h3>
<button onClick={() => setCount(count + 1)}>Click me</button>
</>
);
};
export default UseEffectAnti3;
しかし、こちらのコンポートがレンダリングされるととんでもない速度でカウント数が増えてしまい、無限ループに陥ってしまいます。
問題は
useEffect(() => {
setCount(count + 1);
});
にあります。useEffect
のコールバック関数はコンポーネントがレンダリングされた後に実行されます。コールバック関数内でstate
を更新しているため、実行直後にまたコンポーネントがレンダリングされ、useEffect のコールバック関数が実行され...と無限ループになるわけです。
解決策(3)
依存関係を見直す
シンプルな解決策としては、useEffect
の依存関係を見直すことが挙げられます。先ほどの例ではレンダリングごとにuseEffect
のコールバック関数を実行していたので、これを
useEffect(() => {
setCount(count + 1);
}, []);
のように最初のレンダリングだけ実行されるように修正することで無限ループを回避することができます。
Ref を使う
useRef
を使い参照を更新することで、無限ループを回避することもできます。useEffect
のコールバック関数内で参照値を更新してもコンポーネントは再レンダリングされないのでループは発生しなくなります。
import React, { useEffect, useRef, useState } from "react";
const UseEffectSample: React.FC = () => {
const [count, setCount] = useState(0);
// good pattern 2
const countRef = useRef(0);
useEffect(() => {
countRef.current++;
});
return (
<>
<h3>click count : {countRef.current}</h3>
<button onClick={() => setCount(count + 1)}>Click me</button>
</>
);
};
オブジェクトを依存関係に設定した時 UseEffect が無限ループに陥ってしまう
useEffect
の第二引数の依存配列にオブジェクトを設定した場合、無限ループに陥る可能性があります。
以下のようなコンポーネントを考えてみましょう。
import React, { useEffect, useState } from "react";
const UseEffectAnti5: React.FC = () => {
const [input, setInput] = useState({ value: "", count: 0 });
// anti pattern 5
useEffect(() => {
setInput(c => ({ ...c, count: c.count + 1 }));
}, [input]);
return (
<>
<h3>modify count : {input.count}</h3>
<input
type="text"
value={input.value}
onChange={e => setInput(v => ({ ...v, value: e.target.value }))}
/>
</>
);
};
原因は先ほどの例と同じように、
setInput(c => ({ ...c, count: c.count + 1 }));
にあり、count を 1 増加させていて、この時新しいオブジェクトを作成しています。
そのため、オブジェクト input を依存関係にもつ useEffect の副作用処理はまた呼び出されてしまい、無限ループへとなってしまいます。
解決策(4)
依存関係の変更
先ほどと同じ解決策です。オブジェクトのプロパティを依存配列に持たせましょう。
useEffect(() => {
setInput(c => ({ ...c, count: c.count + 1 }));
}, [input.value]);
こうすることで、入力値が変更された時のみ副作用の処理が呼ばれ、無限ループを回避することができます。
まとめ
以下、useEffect の注意点です。
- useEffect に渡すコールバック関数を Promise にしてはいけない
- 関数コンポーネントはレンダリングされるごとに useEffect をもつ
- useEffect のコールバック関数内で state を更新すると無限ループに陥る可能性がある
useEffect は React Hooks の中でも特に使うシーンが多いと思いますが、しっかり動作を理解して使用しましょう!