Reactのstate更新

# はじめに

少し前までReactの新しいドキュメントがBeta版として公開されていましたが正式オープンとなったようです。
https://react.dev/

公開に関するブログ記事

https://react.dev/blog/2023/03/16/introducing-react-dev

Reactのstateの更新について調べていたところ、とてもわかりやすく書かれており勉強になりました。
今回は特にstateの状態更新関数へ渡す引数として関数を渡した時の挙動についてとその周りについて公式文書を抜粋しながらまとめました。

### 動作確認環境

- Mac Apple M1 Pro バージョン13.1
- Chrome


# Reactのレンダリング

前置きとして、Reactのレンダリングについて少しまとめます。

## レンダリングのトリガー

Reactのレンダリングは、stateの更新がトリガーになります。
例えばボタンのクリックなどを行なった場合、ユーザーイベント(クリック)に直接反応してレンダリングが発生するわけではありません。ボタンのクリックによってレンダリングを行いたい場合は、ボタンのクリックに応じてstateを変化させる必要があります。


以下のコードでは、ボタンをクリックするたびに画面上の表示が更新されます。また、一番最初に画面を開いた時とボタンを押す度にコンソールにログも表示されます。

```react
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)`をコメントアウトすると、ボタンを押しても画面更新はされず、ログも最初の画面表示時のみ表示され、ボタンを押してもログは表示されません。

```react
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になります。

```react
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` に実際の値をマッピングしたコードのイメージは以下のようになります。

```react
const onClick = () => {
  setCount(0 + 1)
  setCount(0 + 1)
  setCount(0 + 1)
}
```

`onClick`内のコード中では`count`の値は0のままです。3回、0に1を足しているため、最終的な結果は1となります。



### 参考

- https://react.dev/learn/render-and-commit
- https://react.dev/learn/state-as-a-snapshot


# 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関数を使って先ほどのコードを書き直します。

```react
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`が実行されると

1.  `setCount(n => n + 1)`： Reactは `n => n + 1` 関数をキューに追加する
1.  `setCount(n => n + 1)`： Reactは `n => n + 1` 関数をキューに追加する
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`には３が設定されます。

表にまとめると以下のようになります。
| キューに入っているupdater関数 | 引数　`n` | 結果 |
|:-:|:-:|:-:|
| n => n + 1 | 0 | 1 |
| n => n + 1 | 1 | 2 |
| n => n + 1 | 2 | 3 |


### 例

`onClick` を次のように書き換えた場合、画面表示される `count` はどのように変化するでしょう？

```react
const onClick = () => {
  setCount(n => n + 1)
  setCount(count + 5)
}
```

ボタンを1回押すと、結果は5になります。
このコードは具体的に以下のように動きます。

1. `setCount(n => n + 1)`: `n => n + 1`関数をキューに追加する
1. `setCount(count + 5)`: 現状の`count`が0のため、「countを5で置き換える」という処理をキューに追加する

キューの中身が実行されると、`count`が０のため、引数`n`に０を渡してupdater関数が実行されます。次に`count`の中身が５に置き換わります。最終的な結果として、画面には５が表示されます。

次にもう一度ボタンを押してみます。すると結果は１０になります。
以下のように動きます。

1. `setCount(n => n + 1)`: `n => n + 1`関数をキューに追加する
1. `setCount(count + 5)`: 現状の`count`が５のため、「countを１０で置き換える」という処理をキューに追加する

キューの中身が実行されると、`count`が5のため、引数`n`に5を渡してupdater関数が実行されます。次に`count`の中身が10に置き換わります。最終的な結果として、画面には10が表示されます。

状態更新関数に何を渡すかによって結果が変わるため、`onClick`のコードの順番を変えるだけでも結果がそれぞれ変わります。

### 参考

- https://react.dev/learn/queueing-a-series-of-state-updates