🐥

なんとなくでuseStateを使うのをやめたい。

2023/10/22に公開

はじめに

なんとなくでuseStateを使っていると、本来stateで管理しなくても良い値にもuseStateを使っていましたが、ReactドキュメントにはどのようにuseStateを使えばいいのか丁寧に解説されており、それを読んで学んだことを備忘録としてまとめます。
https://ja.react.dev/

useStateとは?

useStateはコンポーネントにstate変数を追加するためのフックです。useStateを使うことでReactに対してコンポーネントで管理したい状態を覚えさせることができます。

// 配列内のペアは自由に命名できますが、慣習的に`state`、`setState`のように命名します。
const [state, setState] = useState(initialState);

useStateで管理すべきstateとは?

なんとなくuseStateを使うことをやめるには、何をstateで管理するかを知る必要があります。
ドキュメントによるとstateとは

アプリが記憶する必要のある、変化するデータの最小限のセットのことである

  • 時間が経っても変わらないものですか? そうであれば、state ではありません。
  • 親から props 経由で渡されるものですか? そうであれば、state ではありません。
  • コンポーネント内にある既存の state や props に基づいて計算可能なデータですか? そうであれば、それは絶対に state ではありません!

とあります。

検索用のテキストやチェックボックスなどのユーザー操作による値は時間が経つと変わりますし、既存のstateやpropsから計算することはできないためstateに該当します。

propsで渡された値や、stateやpropsの値を加工したものをstateにいれて管理していましたが、これらは不要なstateということになります。

「Reactの流儀」に詳しく記載されています。
https://ja.react.dev/learn/thinking-in-react#step-3-find-the-minimal-but-complete-representation-of-ui-state

useStateの仕組み

useStateを使う中で、セッター関数で値を更新したのに値が更新されていないように見えることが何度かありました。
例えば以下のようなパターンです。

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        console.log(number) // numberが更新されていない!
      }}>+1</button>
    </>
  )
}

これについてもReactドキュメントに解説があり、

state はスナップショットである
state 変数は、読んだり書いたりできる普通の JavaScript の変数のように見えるかもしれません。しかし、state はむしろ、スナップショットのように振る舞います。state をセットしても、既にある state 変数は変更されず、代わりに再レンダーがトリガされます。

とあります。
上記パターンではボタンをクリックしたときにnumberが直接更新されるように思っていましたが、Reactの動作はそうはなっておらず、以下のような流れで処理が行われます。

  1. onClickイベントハンドラが実行される
  2. setNumber(number + 1)によってnumberに+1した値がセットされ、新しいレンダーを予約する
  3. 新しいnumberの値を使ってコンポーネントを再レンダリングする

レンダーとはReactがコンポーネント(関数)を呼び出すということであり、関数から返されたJSXに含まれるものはレンダー時のstateを使って計算されるようです。
つまり、stateが更新されるたびに新しいstateを使ってコンポーネントを再レンダリングしていることになります。

以下はドキュメントにあるサンプルコードです。
アラートにタイマーを設定して、コンポーネントが再レンダリングされた後に発火するようにした場合、アラートには05のどちらが表示されるでしょうか?

App.js
import { useState } from 'react';
export default function Counter() {
  const [number, setNumber] = useState(0);
  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>
    </>
  )
}

ドキュメントを読む前はsetNumber(number + 5)が実行されるとstateが更新されると思っていたので、当然アラートには5が表示されるものだと思っていました。
しかし、実際にアラートに表示されるのは0です。

stateが「スナップショット」であるということは、レンダリングされた時のstateに固定されており、以下のようにonClick内の処理は以下のように書き換えることができるはずです。

App.js
setNumber(0 + 5);
setTimeout(() => {
  alert(0);
}, 3000);

タイマーを設定しているため、アラートが実行される時にはnumberの値は更新されているかもしれないですが、実際にアラートに渡される値はユーザーがボタンを操作した時点でのstateが使われることになります。

React は、レンダー内の state の値を「固定」し、イベントハンドラ内で保持します。コードが実行されている途中で state が変更されたかどうか心配する必要はありません。

「state はスナップショットである」に詳しく記載されています。
https://ja.react.dev/learn/state-as-a-snapshot

再レンダリング前に最新のstateを読み取る方法

以下のサンプルコードではボタンをクリックするとsetNumber(number + 1)が3回実行されてnumberには3が入ると思うかもしれません。
しかし、Reactではレンダリング時のstateは固定されているので、何度setNumberを呼び出したとしてもnumberは常に0、結果としてはsetNumber(0 + 1)が3回行われ、numberは1となります。

App.js
import { useState } from 'react';
export default function Counter() {
  const [number, setNumber] = useState(0);
  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1); // setNumber(0 + 1)
        setNumber(number + 1); // setNumber(0 + 1)
        setNumber(number + 1); // setNumber(0 + 1)
      }}>+3</button>
    </>
  )
}

次のレンダーまでに同じstateに対して複数回更新したいときはsetNumber(number + 1)ではなく、setNumber(n => n + 1)のようにします。
こうすることで固定された値ではなく1つ前のstateをもとに次のstateを計算することができます。

以下のようにsetNumber(number + 5)の後にsetNumber(n => n + 1)すると、まずnumber + 5によりn5となります。次のsetNumberではn => n + 1なので、number6となります。

<button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
}}>
キュー内の更新処理 n 返り値
0 + 5 0 5
n => n + 1 5 5 + 1 = 6

「一連の state の更新をキューに入れる」に詳しく記載されています。
https://ja.react.dev/learn/queueing-a-series-of-state-updates

まとめ

今まではpropsで受け取った値やそれをもとにコンポーネント内で使う値をuseStateを使って管理していましたが、useStateで管理すべき値は時間経過で変わるもの、すでにある値から再計算できないものなので管理する必要はなかったようです。
stateはレンダリング時に固定される、セッター関数を実行すると再レンダーがトリガーされるということが知れた点が大きな学びだったと思います。
記事内のURL以外にも参考になるものが多くありましたので一度目を通すだけでもかなりの学びにつながると思います。

今回はuseStateについてでしたが、useEffectについても大きな学びがありましたのでそちらもまとめようと思います。

Discussion