📝

[初心者向]Reactスターターはここから始めろ!その3

2024/11/10に公開

前提

当記事はJavaScriptもReactもそんなに詳しくないよって人向けです。

↓↓前記事↓↓
https://zenn.dev/kkyoka/articles/ffe98a29e3c7fa

React公式ドキュメントの「Reactを学ぶ」のstateの管理のところを読んでいきます。
以下はそれのまとめ。
(リデューサ、コンテクストについても書きたかったけど長くなるので別記事にします📖)

Reactは宣言的UI

ReactはUIを宣言的に実装できるようになっている。
従来の命令的UIとは異なり、ユーザーの操作によって何を表示にして何を非表示にして...といちいち指示をする必要がない🧑‍💻

  • 命令的UIとは?
    • 状態変更によるUIのあり方をステップごとに記述するやり方
    • 手順や変化の詳細まで記述する必要がある
    • 従来のJavaScriptではこちらの書き方
  • 宣言型UIは?
    • どの状態のときにどうなるべきか、UIを状態ごとに宣言するやり方
  • なぜ宣言的な実装が可能か?
    • React側で状態の管理(stateの管理)をしており、stateが変わると再レンダーが自動で走るようになっている
    • そのため実装者は状態の変化によってどうUIを変えるかを記述する必要がなく、どの状態のときにどう表示したいかだけを明記するだけで良い🙆‍♀️

Reactでの実装方針の考え方

Reactの実装方針を考える上で、stateをうまく洗い出すことが大事。
必要なstateを洗い出した後、改めて以下を自問すると良い。

  • このstateで矛盾は生じないか?
  • 同じ情報が別のstate変数から入手できないか?
  • 別のstate変数の逆を取って同じ情報を得られないか?
    • 例:isErrorとisSuccessがあった場合、isError=true ⇔ isSuccess=falseとなるためどちらかのみで良さそう

stateの構造の原則

  • 関連するstateをグループ化する
    • 複数のstate変数が常に一緒に変更される場合は、それらを単一のstate変数にまとめると良い
  • stateの矛盾を避ける
    • 例えばbool値を持つ複数のstate変数が同時にtrueになることはない場合、それらをひとつのstate変数にしたほうが良い
  • 冗長なstateを避ける
    • コンポーネントのpropsや既存のstate変数から計算できる情報は、state変数として持つべきではない
  • state内の重複を避ける
    • state変数を別のstate変数が参照する場合、オブジェクトそのものをstate変数に格納するのではなく参照先のidを格納することで値の重複を避ける
    • 例:
      • items = [{ id: 0, title: 'pretzels'}, ...]と、itemsのitemを参照しているselectedItem = {id: 0, title: 'pretzels'}があった場合
      • selectedItemをselectedId = 0に変更して、参照先のIDのみを持つstateにした方が良い
  • 深くネストされたstateを避ける
    • state が簡単に更新できないほどネストしている場合は、「フラット」な構造にするほうが良い
    • DBの正規化と同じ要領で、ネストが深い場合は参照先のIDのみを保持した別stateとして切り出したほうが良い

stateの管理

Reactのバグはstateの誤った管理によるものが多いため、stateを正しく管理する必要がある。
以下はstateの管理方法における基本の考え方およびやり方。

  • state変数は「信頼できる唯一の情報源」の考え方にならい、ひとつのコンポーネントで管理されるべきである
  • コンポーネントの制御性
    • コンポーネントを作成する際は、コンポーネントの中の情報において、何を制御するのか、何を制御しないのかを考える必要がある
      • コンポーネント内で制御する情報:stateで管理
      • コンポーネント内で制御しない情報:propsで管理(親コンポーネントから受け取る)
    • なんでもかんでもstateで管理すれば良いってもんではないよってこと🙅‍♀️
  • stateのリフトアップ
    • 複数のコンポーネントで同じstateを使用したい場合、ひとつ上の親コンポーネントでstate変数を定義し、propsとしてstate変数の値を渡す

stateの仕組み

  • Reactはレンダーツリー内でそのコンポーネントがどの位置にあるかに基づいて、stateとコンポーネントをひもづける
  • UIツリーの中でコンポーネントが同じ位置にレンダーされ続けている間はstate変数は維持される
    • JSXマークアップの位置ではなくUIツリー内の位置でコンポーネントを識別するので注意
    • 以下の場合、flagの値によって別のCounterコンポーネントが配置されるように見えるが、Reactからすれば同じ位置に「Counter」コンポーネントがあると捉えられる
    • そのため、フラグボタン押下時にflagが変更されてもscoreというstate変数の値は維持される
    • export default function App() {
          const [flag, setFlag] = useState(true);
          return (
              <div>
                  {flag ? (
                      <Counter className='hoge' /> 
                  ) : (
                      <Counter className='fuge' /> 
                  )}
              </div>
          );
          <button onClick={() => setFlag(prevFlag => !prevFlag)}>フラグボタン</button>
      }
      
      function Counter({ className }) {
          const [score, setScore] = useState(0);
          return (
              <div className={className}>
                  <h1>{score}</h1>
                  <button onClick={() => setScore(score + 1)}>+1ボタン</button>
              </div>
          );
      }
      
  • コンポーネントが削除されると、それに紐づくstate変数も破棄される
    • 同じ位置のコンポーネントであっても、コンポーネントが異なる場合は別物と判定され、state変数は破棄される
    • コンポーネントが削除された場合、そのコンポーネントにぶらさがるサブツリー全体のstateが破棄される
  • Reactはkeyでもコンポーネントの特定ができる
    • 下記のようにkeyを使用すれば、同じ位置の同じコンポーネントであっても別物として認識される
    • <div>
          {flag ? (
              <Counter key='flagOnComponent' className='hoge' /> 
          ) : (
              <Counter key='flagOffComponent' className='fuge' /> 
          )}
      </div>
      
  • コンポーネントを削除した場合にも、state変数の値を保持する方法
    • コンポーネントを削除するのではなく非表示にする
      • コンポーネントが少ない場合は有用だが、複雑な画面だとDOMノードが増えて処理が重くなる可能性がある
    • stateをリフトアップさせて親コンポーネントに保持させる
      • 最も一般的な解決策である

用語メモ

  • 「信頼できる唯一の情報源」とは
    • システム設計における理論
    • すべてのデータが1箇所でのみ作成、編集されるように構造化する方法
    • 情報源をひとつにすることで、データの矛盾が発生する可能性を軽減させる

感想

stateの洗い出しにおける考え方は、Reactに限ったことではなくDBでステータス系のカラムを追加したいときも同様のことに注意すべきと思った

参照

Discussion