そもそものReactの特徴
stateを使わずに関数を書いた場合、コンポーネント内の関数が複数回実行されても、再レンダリングは起きない。
const Example = () => {
console.log('再レンダリング')
return (
<intput
onchange = {(e) => console.log(e.target.value);}
>
)
}
例えばこちらにinputフォームで、文字を入力したとします。1文字入力するごとにeventが発生し、console.log(e.target.value)が実行されます。しかし、 console.log('再レンダリング')は文字入力とは連動せず、Exampleコンポーネントが最初に呼び出された時のみ実行されます。
stateを使うとどうなる
stateの値が変更されるたびに、コンポーネントが呼び出される。
const Example = () => {
console.log('再レンダリング')
const [text, setText] = useState('');
return (
<intput
onchange = {(e) =>
setText(e.target.value);
console.log(e.target.value);}
>
)
}
文字入力が起きると、onChangenのeventが発生し、setText(e.target.value);によってtextの値が書き換えられる。つまり、1文字入力されるたびに、stateの値が変更される。stateの値が変更されるたびにconsole.log('再レンダリング')が実行される。このようにレンダリングされると、副産物的に関数が実行されてしまい、これを副作用と呼んだりする。
useEffect()を使うとどうなる
結論:起こしたい副作用を指定したり、副作用が起きないように制御できる。
1秒ずつ表示される数字が1ずつ増えるタイマーを例にレンダリングで副作用が起きないようにしてみよう。
まずはuseEffect()を使わないと...
const Timer = () => {
console.log('再レンダリング')
const [time, setTime] = useState(0);
window.setInterval(() => {setTime(prev => prev + 1)}, 1000);
return (
<>{time}</>
)
}
これはまずTimerコンポーネントが呼ばれると、console.log('再レンダリング')が実行される。そして1秒ごとにsetTime(prev => prev + 1)が実行される。ここで注意すべきは、stateが変わると再レンダリングが起きるということ。つまり、setTime(prev => prev + 1)によってstateの値が1秒ごとに変わると再レンダリングが起きてtimeの値がおかしくなる。
どうすればいいかというと、stateの値が変わっても、再レンダリングが起きないようにすればいい。言い換えると、コンポーネントが呼ばれたときに、関数が実行されるのは1度だけになるようにすればいい。
const Timer = () => {
console.log('再レンダリング')
const [time, setTime] = useState(0);
useEffect(() => {
console.log('useEffectが呼ばれたぜ')
window.setInterval(() => {setTime(prev => prev + 1)}, 1000)
},[])
return (
<>{time}</>
)
}
window.setIntervalをuseEffect()の中に入れることで、Timerコンポーネントが呼ばれた時に一度だけ実行されるようになる。
useEffect()の第二引数の配列について
第二引数には配列が入る。この配列にstateを含めると、そのstateが変更された時にuseEffect()の第一引数にあるコールバック関数が実行される。
useEffect(callback関数, [state1, state2,....]);
Timerの例のように、コンポーネントが呼ばれたら1度だけ関数を実行すれば良い場合は、配列は空にする。
まとめ
useEffect()はstateの更新時のレンダリングによって実行される副産物的な関数実行(副作用)を制御する役割がある。2回目以降のレンダリングで副作用を起こしたくないときは、useEffect()の第2引数に空配列[]を渡せば良い。