React のドキュメントを読む③ - state の管理
state を使って入力に反応する
宣言型 UI と命令型 UI の比較
命令型プログラミングでは、起こったことに応じて UI を操作するための命令そのものを書く。
別の考え方をしてみると、タクシーの運転手に、「曲がるたびに行き先を指示する」ようなもの。運転手はあなたがどこに行きたいのか知らず、ただあなたの指示に従うだけ。
一方、React のような宣言型プログラミングでは UI を直接操作することはない。
つまり、コンポーネントの有効化、無効化、表示、非表示を直接行うことはなく、代わりに表示したいものを宣言することで、React が UI を更新する方法を考えてくれる。
タクシーに乗ったとき、どこで曲がるかを正確に伝えるのではなく、どこに行きたいかを運転手に伝えることを思い浮かべてみるとわかりやすい。
運転手はあなたをそこに連れていくのが仕事だし、あなたが考えもしなかった近道も知っているかもしれない。
state 構造の選択
state 構造の原則
state を格納するコンポーネントを作成する際に、いくつ state 変数を使うのか、データ構造をどのようにするのかについて選択を行う必要がある。以下はより良い選択をするために役立つ原則。
-
関連する state をグループ化する
- 2 つ以上の state 変数を常に同時に更新する場合、それらを単一の state 変数にまとめることを検討
-
state の矛盾を避ける
- state の複数部分が矛盾して互いに「衝突する」構造になっている場合、ミスが発生する余地があるため、避ける
-
冗長な state を避ける
- コンポーネントの props や既存の state 変数からレンダー時に何らかの情報を計算できる場合、その情報をコンポーネントの state に入れるべきではない
-
state 内の重複を避ける
- 同じデータが複数の state 変数間、またはネストしたオブジェクト間で重複している場合、それらを同期させることは困難なため、できる限り重複を減らす
-
深くネストされた state を避ける
- 深い階層構造となっている state はあまり更新しにくいため、できる限り state をフラットに構造化する方法を選ぶ
また、props を state にコピーするのも混乱の元になるため避けるべきである。
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
ここでは、color という state 変数が props である messageColor の値で初期化されている。
問題は、親コンポーネントが後で異なる messageColor 値(例えば 'blue' から 'red')を渡してきた場合、state 変数である color の方は更新されないということ。
state は最初のレンダー時にのみ初期化されるためである。
代わりに、messageColor をコードで直接使用する。
function Message({ messageColor }) {
const color = messageColor;
これにより、親コンポーネントから渡された props と同期されなくなってしまうことを防げる!
props を state に「コピー」することが意味を持つのは、特定の props のすべての更新を意図的に無視したい場合だけ。慣習として、新しい値が来ても無視されるということを明確にしたい場合は、props の名前を initial または default で始めるようにする。
function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);
state の保持とリセット
ポイントは、React は、同じコンポーネントが同じ位置でレンダーされている限り、state を保持するという点。
これは、state は JSX タグに保持されるのではなく、JSX を置くツリー内の位置に関連付けられているためである。
stateをリセットしたい場合、異なる key を与えることで、サブツリーの state をリセットするよう強制することができる。
詳細はこちらのページを参照する。
state ロジックをリデューサに抽出する
useState と useReducer の比較
-
コードサイズ
- 一般に、useState を使った方が最初に書くコードは少なくなる。
- useReducer の場合、リデューサ関数とアクションをディスパッチするコードを両方書く必要がある。
- ただし、多くのイベントハンドラが同様の方法で state を変更している場合、useReducer によりコードを削減できる。
-
可読性
- シンプルな state 更新の場合は useState を読むのは非常に簡単。
- しかし、より複雑になると、コンポーネントのコードが肥大化し、見通すことが難しくなる。このような場合、useReducer を使うことで、更新ロジックによって書かれる「どう更新するのか」と、イベントハンドラに書かれる「何が起きたのか」とを、きれいに分離することができる。
-
デバッグ
- useState を使っていてバグがある場合、state がどこで誤ってセットされたのか、なぜそうなったかを特定するのが難しくなることがある。
- useReducer を使えば、リデューサにコンソールログを追加することで、すべての state 更新と、それがなぜ起こったか(どの action のせいか)を確認できる。それぞれの action が正しい場合、リデューサのロジック自体に問題があることが分かる。ただし、useState と比べてより多くのコードを調べる必要がある。
-
テスト
- リデューサはコンポーネントに依存しない純関数。これは、リデューサをエクスポートし、他のものとは別に単体でテストできることを意味する。
- 一般的には、より現実的な環境でコンポーネントをテストするのがベストだが、複雑な state 更新ロジックがある場合は、特定の初期 state とアクションに対してリデューサが特定の state を返すことをテストすることが役立つ。
-
個人の好み
- 人によってリデューサが好きだったり、好きではなかったりする好みの問題。
- useState と useReducer の間を行ったり来たりすることはいつでも可能。どちらも同等のもの!
コンテクストで深くデータを受け渡す
コンテクスト:props 受け渡しの代替手段
コンテクストを使うことで、親コンポーネントが配下のツリー全体にデータを提供できる。
コンテクストを使う前に
コンテクストを使う前に検討すべきこと。
- まずは props を渡す方法から始める
-
コンポーネントを抽出して、children を JSX として渡す方法を検討する
- もし、何らかのデータを、それを必要とせずただ下に流すだけの中間コンポーネントを何層も経由して受け渡ししているような場合、何かコンポーネントを抽出するのを忘れているということかもしれない
- たとえば、<Layout posts={posts} /> のような形で、データを直接使わないビジュアルコンポーネントに post のようなデータを渡しているのかもしれない
- 代わりに、Layout は children を props として受け取るようにし、<Layout><Posts posts={posts} /></Layout> のようにレンダーしてみる
- これにより、データを指定するコンポーネントとそれを必要とするコンポーネントの間のレイヤ数が減る
これらのアプローチがどちらもうまくいかない場合は、コンテクストを検討する。