useEffectについて
以下をもとに理解を進めます
// 1. インポート部分
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { RepeatClockIcon as ResetIcon } from '@chakra-ui/icons';
import { Box, Button, Stat, StatLabel, StatNumber } from '@chakra-ui/react';
// 2. プロップの型定義とデフォルト値
type Props = { maxCount?: number };
const MAX_COUNT = 60;
// 3. コンポーネントの定義
const Timer: FC<Props> = ({ maxCount = MAX_COUNT }) => {
// 4. Stateの定義
const [timeLeft, setTimeLeft] = useState(maxCount);
// 5. ヘルパー関数の定義
const tick = (): void => setTimeLeft((t) => t - 1);
const reset = (): void => setTimeLeft(maxCount);
// 6. useEffectフック
useEffect(() => {
const timerId = setInterval(tick, 1000);
return () => clearInterval(timerId);
}, []);
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
});
// 7. レンダリング
return (
<Box p={5} w="sm" borderWidth="1px" borderRadius="lg" boxShadow="base">
<Stat mb={2}>
<StatLabel fontSize={18}>Count</StatLabel>
<StatNumber fontSize={42}>{timeLeft}</StatNumber>
</Stat>
<Button w="xs" colorScheme="red" variant="solid" leftIcon={<ResetIcon />} onClick={reset}>
Reset
</Button>
</Box>
);
};
export default Timer;
このコンポーネントは、指定された最大時間からカウントダウンを開始し、時間が0になったらリセットボタンを押すことで時間を再設定するか、自動的に再設定される機能を持つタイマーコンポーネントです。
以下、ESLintでの警告が出る。
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
});
警告の内容として、setTimeLeft
を使うuseEffect
の中に依存配列が無いため、無限の再レンダリングが発生する可能性があるというものでした。
この警告は、timeLeft
やmaxCount
の値に基づいて何らかの副作用を起こしているのに、それらの変数を依存配列に追加していないために発生しています。
ステップ1: 依存配列を追加する
まず、警告に従い、timeLeft
とmaxCount
を依存配列に追加します。
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
}, [timeLeft, maxCount]);
ステップ2: 無限ループの理解
ここで疑問が生まれるかもしれません:「なぜこれが無限の再レンダリングにつながる可能性があるのか?」
setTimeLeft(maxCount)
が実行されると、timeLeft
の値がmaxCount
にリセットされます。これにより、コンポーネントは再レンダリングされます。依存配列が設定されていないと、このuseEffect
は再レンダリングの度に毎回実行されるため、timeLeft
が0でなくてもsetTimeLeft(maxCount)
が実行される可能性があります。これは、期待しない挙動や無限の更新を引き起こす可能性があります。
ステップ3: 依存配列の重要性
依存配列に変数を追加することで、その変数の値が変更されたときにのみuseEffect
内の副作用が実行されるようになります。この例では、timeLeft
が0になったとき、またはmaxCount
が変更されたときにのみ副作用が実行されます。
このように、依存配列を正確に設定することで、期待する動作を安全に実現することができ、不要な副作用の実行や無限の再レンダリングのリスクを回避することができます。
useEffect と再レンダリング
React では、state または props の変更があると、コンポーネントは再レンダリングされます。useEffect
は、コンポーネントがレンダリングされるたびにデフォルトで実行されます。ただし、useEffect
の第2引数(依存配列)を使用することで、特定の変数の変更時のみ副作用を実行するように制限することができます。
依存配列がない場合
もしuseEffect
が以下のように書かれていた場合:
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
});
このuseEffect
は、コンポーネントが再レンダリングされるたびに実行されます。そして、timeLeft
が0のとき、setTimeLeft(maxCount)
が実行され、timeLeft
の値がmaxCount
に更新されます。この更新により、再度コンポーネントが再レンダリングされます。しかし、この新しいレンダリングにおいても、上記のuseEffect
は再び実行されます。
無限ループのリスク
上記のuseEffect
の設定では、timeLeft
が0でなくても、再レンダリングが発生するたびにuseEffect
が実行されるため、意図しない更新や無限ループが発生するリスクがあります。
例えば、timeLeft
が0でないにもかかわらず、他の理由でコンポーネントが再レンダリングされた場合、上記のuseEffect
は毎回実行され、結果としてsetTimeLeft(maxCount)
が実行される可能性があります。
依存配列の利点
これを避けるためには、依存配列を使用して、timeLeft
またはmaxCount
が変更されたときのみuseEffect
が実行されるようにする必要があります:
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
}, [timeLeft, maxCount]);
このように設定することで、timeLeft
またはmaxCount
の値が変わったときのみ副作用が実行され、不要な更新や無限ループのリスクを防ぐことができます。
「コンポーネントが再レンダリングされる」というのは、コンポーネントのstate
やprops
が変更されたとき、または親コンポーネントが再レンダリングされたときなど、さまざまな要因でコンポーネントのレンダリングが再度行われることを指します。
useEffect
はデフォルトでコンポーネントの再レンダリングが行われるたびに実行されます。しかし、useEffect
の第2引数(依存配列)を指定することで、特定の変数の変更時だけ副作用が実行されるように制限することができます。
したがって、例として挙げたuseEffect
が
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
});
このようになっている場合、このuseEffect
はコンポーネントが再レンダリングされるたび(state
やprops
の変更など、どんな要因であれ)に実行されます。これが無限ループのリスクを生む要因となります。
逆に、依存配列を持つuseEffect
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
}, [timeLeft, maxCount]);
は、timeLeft
またはmaxCount
の値が変わった場合のみ実行されます。それ以外の再レンダリングの要因で実行されることはありません。
必要なタイミングで以下を読むこと
読んでみた