React.jsのオフィシャルサイトのLearn React を読み直してみた

React.jsのオフィシャルサイトのLearn React (https://ja.react.dev/learn) を改めて読み直したました。

Reactを学習し始めたときだけでなく、Reactに慣れてきたときこそ読み直すとまた新たな学びがありました。より深くReactを理解して使いこなすためにもぜひ読み直してみてください。

Reactのコンポーネントの特徴

UIを宣言的に書くことができる

  • 命令形UI(個々のUI部分を直接操作)ではなく宣言型UI(UIがとりうる状態を記述)で書く
  • 命令形UIでは、UIの構築から操作が相互に絡みあうため複雑なシステムになるほど指数関数的に変更が難しくなる
  • Reactでは、表示したいUIを宣言的に記述できるようにすることでこの問題を解決している
  • Ref: https://ja.react.dev/learn/reacting-to-input-with-state

関数型言語の純粋関数の思想で設計されている

Reactコンポーネントレンダリングステップ

Reactコンポーネントレンダリングされる場合、「トリガー」→「レンダー」→「コミット」のステップで画面に反映される。

コンポーネントのフェイズ(引用元:Learn React illustrated by Rachel Lee Nabors)

  1. トリガー : Reactのレンダーをトリガーする
  2. 以下2ケースでレンダーがトリガーされれる
  3. コンポーネントの初期レンダーとき
  4. steteが更新されたとき
  5. レンダー : Reactはコンポーネントを呼び出して、画面に何を表示するか決定する
  6. レンダリングとはReactがコンポーネントを呼び出すこと
  7. コンポーネントはツリー構造になっているので、レンダーは再帰的に呼び出される
  8. 初期レンダーの場合、Reactはルートコンポーネントを呼び出す
  9. 再レンダーの場合、stateが更新されたコンポーネントを呼び出して、差分を計算する
  10. コミット : ReactはDOMに変更を反映する

Ref: https://ja.react.dev/learn/render-and-commit

ReactでのUIを構築する思考プロセス

ja.react.dev

ReactでUIを構築する場合、思考プロセスを変化させる必要がある。

UIをコンポーネントの階層に分割(引用元:Learn React)

具体的には以下の流れでUIを構築していく。

  1. まず、UIをコンポーネントの階層に分割する
  2. 次に、Reactで静的なバージョンを作成する
  3. そして、UIの視覚的な状態のデータモデルを識別する
  4. 最後に、stateやイベントハンドラーを利用して複数のコンポーネントを接続しデータが流れるようにする

state の管理

state で状態を管理する

  • UIは状態に応じて視覚的な状態が変わる(例: 初期状態、入力中、成功状態、失敗状態など)
  • 状態を表すためにuseStateで state を宣言する
  • ユーザ操作に応答するイベントハンドラから state を操作して状態を変化させる
  • Ref: https://ja.react.dev/learn/managing-state

state の構造の原則

  • ミスを入り込ませず state を用意に更新できるようにするためstateの構造を適切に設計する
  • 関連するstateをグループ化する:常に同時にstate変数を更新する場合はまとめられないか?
  • stateの矛盾を避ける:state同士が矛盾しえる場合はまとめられないか?
  • 冗長なstateを避ける:既存のstateやpropsから計算できるならstateにいれるべきではない
  • state内の重複を避ける:同じデータが複数のstate変数に重複していると同期は大変なのでできるだけ重複を減らす
  • 深くネストされたstateを避ける:深い階層構造のstateは更新しづらいので可能な限りstateをフラットに構造化する方法を選ぶ
  • Ref: https://ja.react.dev/learn/choosing-the-state-structure

state の更新ロジックを Reducer に抽出する

ja.react.dev

  • Reducer にstateの更新ロジックを集約できる
  • 多くのイベントハンドラにまたがって同じ state を更新をする場合はReducerに集約するとコードを理解しやすくなる

良い Reducer の書き方

  • Reducer は純粋関数にする
    • Reducerはレンダリング中に呼ばれるので純粋関数の必要がある
    • Reducerの関数内でリクエストを送信したり、タイムアウトを設定などの副作用は実行しない
    • また、state を直接更新せずコピーすることでイミュータブルに扱う
  • 各アクションは単一のユーザ操作で記述する
    • 複数のデータを更新する場合でも、ユーザー操作が1つであればアクションは1つにする
    • 例えば、フォームのリセットボタンがあった場合に、5つのフィールドのstateを更新するが操作は1つなので、"reset_form"というようなアクションが1つあればよい

useStateuseReducerの比較

  • 複雑な更新関数だったり複数箇所から呼ばれるなら Reducer の利用を選択肢にいれるとよさそう
  • コードサイズ:単一箇所ならuseState。複数箇所から呼ばれるならuseReducerのほうがコードは削減できる
  • 可読性:シンプルな更新関数ならuseSate。複雑な更新関数ならuseReducerで可読性があがる
  • デバッグuseReducerはstateの更新箇所が一箇所なのでそこにログを仕込めばデバッグしやすい?
  • テスト:Reducerは純粋関数なのでテストがしやすい
  • 個人の好み:Reducerが好きだったり好きでなかったり好みがある

Context でデータを受け渡す

ja.react.dev

  • Reactは親コンポーネントから子コンポーネントに props を使って情報を渡す
  • 多くのコンポーネントが同じ情報を必要とする場合、propsの受け渡しは冗長なことがある
  • Context を利用することで、明示的に props を渡さなくても任意のコンポーネントが情報を受け取れるようになる
  • 暗黙的になるので、Contextを使いすぎると可読性が悪くなるので利用は慎重に

Context の利用用途例

  • テーマ:ダークモードなどのアプリの外観をユーザーが変更できるようにする場合。外観を変化させる必要があるコンポーネントでContextを利用する
  • ログイン中アカウント:多くのコンポーネントは現在ログインしちえるユーザーを知る必要がある。それを Context にいれることで、ツリーのどこからでも読み取りしやすくなる。
  • ルーティング:多くのルーティングの実現方法として、現在のルートを保持するために内部で Context を利用している。
  • state管理:アプリが大きくなるとstateのリフトアップによりアプリトップ近くに大量のstateが集まってくることがある。ReducerとContextを一緒に利用することで複雑なstateを離れたコンポーネントに受け渡すことができる

Context を使う前に考えたいこと

  • Contextは魅力的だが、使い過ぎは注意が必要
  • いくつかのpropsを数レベルの深さに渡って受け渡す必要があるというだけでContextを使うべきではない
  • まずは、propsを渡す方法から始める
    • 少し凝ったコンポーネントの場合、多くのpropsを多くのコンポーネントを通して受け渡すことは珍しくない
    • propsの受け渡し(データの流れ)が明示的になっているので、コンポーネントがどのデータを使っているか明確になるのでコードがメンテしやすい
  • コンポーネントの抽出を検討する
    • 例えば、ビジュアルコンポーネントの場合はchildrenを使って抽出できるかもしれない
    • <Layout posts={posts /><Layout><Posts posts={posts} /></Layout>

refで値を参照する

ja.react.dev

  • useRefでrefを宣言できる
  • refの現在の値はref.currentプロパティを通してアクセスできる
  • refの値はミュータブルで値を変更しても再レンダリングされない

refを使うタイミング

  • refを使用するのは、コンポーネントがReactの外に踏み出して外部システムやブラウザAPIと連携する場合に使う
  • タイムアウトIDの保存
  • DOM要素の保存と操作(フォーカスを当てる、スクロール、Reactが公開していないブラウザAPIの呼び出しなど)
  • JSXを計算するために必要でないその他のオブジェクトの保存

refのベストプラクティス

  • refを避難ハッチとして扱う
    • refが有用なのは、外部システムやブラウザAPIと連携する場合
    • データフローの多くが ref に依存している場合は何かが間違っている
  • レンダー中に ref.current を読み書きしない
    • Reactは ref.current が書き換わったタイミングを把握しないため、コンポーネントの挙動が予測しづらくなる
    • 通常、refにアクセスするのはイベントハンドラから使用する
    • レンダー中に情報が必要な場合は state を使用する

Effectを使って外部システムと同期する

ja.react.dev

  • Effectはレンダー自体によって引き起こされる副作用を指定するためのもの
  • Effectはコンポーネントのコミットの最後に画面が更新された後に実行される
  • useEffectで副作用(side effect)を宣言できる

Effectのユースケース

  • React以外のUIウィジットの制御
  • イベントリスナー登録と解除
  • アニメーションのトリガー
  • サーバからのデータのフェッチ
  • 分析ログの送信

Effectのライフサイクル