はじめに
少し前までReactの新しいドキュメントがBeta版として公開されていましたが正式オープンとなったようです。
https://react.dev/
公開に関するブログ記事
Reactのstateの更新について調べていたところ、とてもわかりやすく書かれており勉強になりました。
今回は特にstateの状態更新関数へ渡す引数として関数を渡した時の挙動についてとその周りについて公式文書を抜粋しながらまとめました。
動作確認環境
- Mac Apple M1 Pro バージョン13.1
- Chrome
Reactのレンダリング
前置きとして、Reactのレンダリングについて少しまとめます。
レンダリングのトリガー
Reactのレンダリングは、stateの更新がトリガーになります。
例えばボタンのクリックなどを行なった場合、ユーザーイベント(クリック)に直接反応してレンダリングが発生するわけではありません。ボタンのクリックによってレンダリングを行いたい場合は、ボタンのクリックに応じてstateを変化させる必要があります。
以下のコードでは、ボタンをクリックするたびに画面上の表示が更新されます。また、一番最初に画面を開いた時とボタンを押す度にコンソールにログも表示されます。
import { useState } from "react";
export default function App() {
console.log("===== render =====")
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={onClick}>Click!</button>
<p>count is {count}</p>
</div>
)
}
onClickの中のsetCount(count + 1)をコメントアウトすると、ボタンを押しても画面更新はされず、ログも最初の画面表示時のみ表示され、ボタンを押してもログは表示されません。
import { useState } from "react"
export default function App() {
console.log("===== render =====")
const [count, setCount] = useState(0)
const onClick = () => {
// setCount(count + 1)
}
return (
<div>
<button onClick={onClick}>Click!</button>
<p>count is {count}</p>
</div>
)
}
stateが保持する値の更新タイミング
stateの更新を行なった場合、その変更が反映されるのは次のレンダリングプロセスです。そのため、例えばonClick関数内で複数回stateを更新したとしても、その更新は反映されません。
以下のコードではボタンを1回押すと、countは3ではなく1になります。
import { useState } from "react"
export default function App() {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
return (
<div>
<button onClick={onClick}>Click!</button>
<p>count is {count}</p>
</div>
)
}
count に実際の値をマッピングしたコードのイメージは以下のようになります。
const onClick = () => {
setCount(0 + 1)
setCount(0 + 1)
setCount(0 + 1)
}
onClick内のコード中ではcountの値は0のままです。3回、0に1を足しているため、最終的な結果は1となります。
参考
stateの更新
本題のstateの更新についてです。
stateの更新は、上のコードでも見たように、useStateの戻り値で得られる、状態更新関数(上記例ではsetCount)を利用します。
バッチ処理
Reactでは、イベントハンドラー内のすべてのコードが実行されるまで、stateの更新を待機します。先ほどのonClick内でsetCountが3回呼ばれるコードでは、すべてのsetCountが呼び出された後にのみ、再レンダリングが行われます。
このバッチ処理と呼ばれる挙動により、再レンダリングの数を減らし、Reactアプリの実行を大幅に高速化しています。また、一部のstateのみが更新されたような中途半端な状態での再レンダリングが行われるといった混乱も避けられます。ただしこれは、イベントハンドラーとその中のコードが完了するまでUIが更新されないということを意味します。
ちなみに、Reactは複数のイベントにまたがってバッチ処理を行うことはありません。各クリックは、クリックごとに個別に処理されます。
状態更新関数に指定できるもの
状態更新関数には、上で見てきたような値(上記例では数値)を引数として渡すことができます。例えばsetCount(2)です。このsetCount(2)のコードは、Reactに対して 「count変数の中身を 2 に置き換えて」と指示することになります。
そのほかに関数を渡すこともできます。この関数をupdater関数と呼ぶことにします。例えばsetCount(n => n + 1)です。この中のn => n + 1をupdater関数と呼びます。updater関数の引数nはキューに存在する前のstateの値を受け取ります。そのため、setCount(n => n + 1)はReactに対して「前の値を使ってupdater関数の処理を実行して」と指示するイメージとなります。
このupdater関数を使って先ほどのコードを書き直します。
import { useState } from "react"
export default function App() {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(n => n + 1)
setCount(n => n + 1)
setCount(n => n + 1)
}
return (
<div>
<button onClick={onClick}>Click!</button>
<p>count is {count}</p>
</div>
)
}
上記コードではボタンを1回押すと、画面には 3 と表示されます。
具体的には次のように動作します。
onClickが実行されると
-
setCount(n => n + 1): Reactはn => n + 1関数をキューに追加する -
setCount(n => n + 1): Reactはn => n + 1関数をキューに追加する -
setCount(n => n + 1): Reactはn => n + 1関数をキューに追加する
onClick内のコードがすべて実行されると、Reactはこのキューの中身を順番に実行していきます。
元々のcountの値は0のため、引数nに0を渡して最初のupdater関数を実行します。次にその結果である1を次のupdater関数の引数として渡し、2個目のupdater関数が実行されます。最後に3つ目のupdater関数が引数2で実行されます。
最終的な値は3となり、countには3が設定されます。
表にまとめると以下のようになります。
| キューに入っているupdater関数 | 引数 n
|
結果 |
|---|---|---|
| n => n + 1 | 0 | 1 |
| n => n + 1 | 1 | 2 |
| n => n + 1 | 2 | 3 |
例
onClick を次のように書き換えた場合、画面表示される count はどのように変化するでしょう?
const onClick = () => {
setCount(n => n + 1)
setCount(count + 5)
}
ボタンを1回押すと、結果は5になります。
このコードは具体的に以下のように動きます。
-
setCount(n => n + 1):n => n + 1関数をキューに追加する -
setCount(count + 5): 現状のcountが0のため、「countを5で置き換える」という処理をキューに追加する
キューの中身が実行されると、countが0のため、引数nに0を渡してupdater関数が実行されます。次にcountの中身が5に置き換わります。最終的な結果として、画面には5が表示されます。
次にもう一度ボタンを押してみます。すると結果は10になります。
以下のように動きます。
-
setCount(n => n + 1):n => n + 1関数をキューに追加する -
setCount(count + 5): 現状のcountが5のため、「countを10で置き換える」という処理をキューに追加する
キューの中身が実行されると、countが5のため、引数nに5を渡してupdater関数が実行されます。次にcountの中身が10に置き換わります。最終的な結果として、画面には10が表示されます。
状態更新関数に何を渡すかによって結果が変わるため、onClickのコードの順番を変えるだけでも結果がそれぞれ変わります。