React.jsのオフィシャルサイトのLearn React を読み直してみた
React.jsのオフィシャルサイトの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はコンポーネントが純粋関数であると仮定して動く
- 副作用はレンダー外に動くイベントハンドラや
useEffect
で書く - Ref: https://ja.react.dev/learn/keeping-components-pure
Reactコンポーネントのレンダリングステップ
Reactコンポーネントがレンダリングされる場合、「トリガー」→「レンダー」→「コミット」のステップで画面に反映される。
- トリガー : Reactのレンダーをトリガーする
- 以下2ケースでレンダーがトリガーされれる
- コンポーネントの初期レンダーとき
- steteが更新されたとき
- レンダー : Reactはコンポーネントを呼び出して、画面に何を表示するか決定する
- レンダリングとはReactがコンポーネントを呼び出すこと
- コンポーネントはツリー構造になっているので、レンダーは再帰的に呼び出される
- 初期レンダーの場合、Reactはルートコンポーネントを呼び出す
- 再レンダーの場合、stateが更新されたコンポーネントを呼び出して、差分を計算する
- コミット : ReactはDOMに変更を反映する
Ref: https://ja.react.dev/learn/render-and-commit
ReactでのUIを構築する思考プロセス
ReactでUIを構築する場合、思考プロセスを変化させる必要がある。
具体的には以下の流れでUIを構築していく。
- まず、UIをコンポーネントの階層に分割する
- 次に、Reactで静的なバージョンを作成する
- そして、UIの視覚的な状態のデータモデルを識別する
- 最後に、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 に抽出する
- Reducer にstateの更新ロジックを集約できる
- 多くのイベントハンドラにまたがって同じ state を更新をする場合はReducerに集約するとコードを理解しやすくなる
良い Reducer の書き方
- Reducer は純粋関数にする
- Reducerはレンダリング中に呼ばれるので純粋関数の必要がある
- Reducerの関数内でリクエストを送信したり、タイムアウトを設定などの副作用は実行しない
- また、state を直接更新せずコピーすることでイミュータブルに扱う
- 各アクションは単一のユーザ操作で記述する
- 複数のデータを更新する場合でも、ユーザー操作が1つであればアクションは1つにする
- 例えば、フォームのリセットボタンがあった場合に、5つのフィールドのstateを更新するが操作は1つなので、"reset_form"というようなアクションが1つあればよい
useState
とuseReducer
の比較
- 複雑な更新関数だったり複数箇所から呼ばれるなら Reducer の利用を選択肢にいれるとよさそう
- コードサイズ:単一箇所なら
useState
。複数箇所から呼ばれるならuseReducer
のほうがコードは削減できる - 可読性:シンプルな更新関数なら
useSate
。複雑な更新関数ならuseReducer
で可読性があがる - デバッグ:
useReducer
はstateの更新箇所が一箇所なのでそこにログを仕込めばデバッグしやすい? - テスト:Reducerは純粋関数なのでテストがしやすい
- 個人の好み:Reducerが好きだったり好きでなかったり好みがある
Context でデータを受け渡す
- 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で値を参照する
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 を使用する
- Reactは
Effectを使って外部システムと同期する
- Effectはレンダー自体によって引き起こされる副作用を指定するためのもの
- Effectはコンポーネントのコミットの最後に画面が更新された後に実行される
useEffect
で副作用(side effect)を宣言できる
Effectのユースケース
- React以外のUIウィジットの制御
- イベントリスナー登録と解除
- アニメーションのトリガー
- サーバからのデータのフェッチ
- 分析ログの送信
Effectのライフサイクル
- ReactコンポーネントとEffectのライフサイクルは異なる
- Reactコンポーネントは、マウント、更新、マウントの3つ
- Effectは、開始と停止の2つ
- Ref: https://ja.react.dev/learn/lifecycle-of-reactive-effects