⏱️

『状態』にする前に少し考えよう

2023/02/17に公開

Reactの状態管理ライブラリーは群雄割拠の時代ですね。

  • Jotai
  • Recoil
  • Zustand
  • Redux(Toolkit)
  • XState etc..

どの状態管理ライブラリーにも共通する話をします。「状態」との付き合い方について、状態を設計するときの基礎的な考え方について書きたいと思います。状態管理ライブラリー初心者の人には是非読んでもらいたい内容です。

まず、状態管理は大変だよね?

データを状態として扱うということは、言い換えるとデータをミュータブルに扱うということです。ミュータブルなデータはイミュータブルなデータよりも考えなければいけないことが多く、管理するのが大変です。

「状態管理は大変」と言われて、状態の扱いにすっかり慣れてしまった人たちには響かないかもしれません。でも、ReactやVueを使う時代になり、かなり大きな状態管理から解放されています。

『いやいや、ReactはuseState、Vueはref(Vue3, setup構文)で状態を持ちますけど』
確かに。

でも、ほとんど扱わなくて済むようになった『状態』があります。それはDOMの状態です。

ReactやVueでは宣言的にDOM構造を記述します。これは仮想DOMという概念で、ライブラリーはブラウザーが管理しているDOM(実際のDOM)との差分を検出し、ライブラリーがDOM操作することで仮想DOMと同期させてくれます。

これはつまりディアボロのキング・クリムゾンです。

『結果』だけだ!! この世には『結果』だけが残る!!

あるべきDOM構造について記述すれば、DOM操作という過程は消え去り、実際のDOMだけが残る!

仮想DOMをもっと詳しく知りたい方は、こちらを読むしかないでしょう。
https://qiita.com/mizchi/items/4d25bc26def1719d52e6

実際のDOMが持つ「状態」という側面を意識しなくなり、仮想DOMという「結果」としてDOMを扱えるようになりました。つまりReactやVueは、DOMを状態として扱うことから解放してくれたのです。

「状態」ではなく「結果」として考えられないか?

ReactやVueでは、useStateref、あるいは状態管理ライブラリーによる状態にさえ注意を払っておけば、実際のDOMを状態として意識することなく開発出来て幸せ、という話でした。

この考えを広く適用してコードを組み立てるとスッキリします。つまり状態管理は大変だから、注意を向けなければならない状態を減せばいいのです。

この考えをライブラリー開発者が意識しているからこそ、私が知っているほとんどの状態管理ライブラリーはセレクター[1]と呼ばれる仕組みを用意しています。

状態管理ライブラリーを使っていたら、状態として扱っているものを確認してみてください。状態として扱っているもののうち、セレクターの仕組みを活用して他の状態の計算結果として扱えるものはないか、探しましょう。

基本編: errorMessageは状態?

私の経験から1つ例を紹介します。

チーム開発をしていて、バリデーションエラーが残ってしまうバグをデバックすることになりました。コードを読んでいくと、状態管理ライブラリーPiniaのstoreに格納するStateが次のように定義されていました。

type State = {
  value: number | null;
  valueErrorMessage?: string;
  ...
}

フォームの値を格納するときに、バリデーションして表示したいエラーメッセージも格納していました。

フォームの値の変更経路が1つの場合は必ずvalueErrorMessageも上書きされてバグにならなかったと思いますが、リセットボタンなどフォームの値の変更経路が複数の場合に、それらすべての処理でvalueErrorMessageの更新を忘れないよう処理を記述しなくれはなりません。

こういった状況はつらいですね。「すべての...を忘れないよう」と考える時点でアウトです。忘れちゃいけないことを出来るだけ少なくしておくこと。これが素早くバグの少ないことコードを書くために大切だと思います。

リファクタリングの手順としては、まずStateに定義したvalueErrorMessageを消します。Piniaはgetterというセレクターがあるのでvalueから計算させるvalueErrorMessageというgetterを定義します。これですべて解決です。valuevalueErrorMessageの不整合は発生しなくなりました。

この例では、次のようなパターンの場合に管理すべき状態を減らせる、という話でした。

result = f(value)

複数の状態から結果を計算するパターンも今回のテクニックが適用できます。

result = f(value1, value2, value3)

「サーバーに問い合わせないとエラーかどうかわからない値の場合は?」

どうしましょうか。

応用編: キャッシュは管理すべき状態ではない

取得したデータを操作する予定がない場合は状態として扱うのを避けましょう。
状態ではなくキャッシュとして扱うことで注意力という貴重なリソースをセーブできます。

キャッシュとして扱うテクニックを2つ紹介します。

セレクターにキャッシュさせる

もし、セレクターがキャッシュ機能を備えているなら、サーバーから取得した値すら状態管理ライブラリーのstoreに格納する必要はありません。そのことを詳細に解説してくれている記事を紹介します。

https://zenn.dev/uhyo/articles/recoil-selector-infinite-scroll

使用する状態管理ライブラリーの機能をよく知っておかないとこういったコードは書けないですね。

https://recoiljs.org/docs/api-reference/core/selector/#cache-policy-configuration

フェッチライブラリーにキャッシュさせる

最近のフェッチライブラリーはキャッシュ機能を持っていたりします。

https://zenn.dev/mutex_inc/articles/react-query-state-mgmt

こういったライブラリーを積極活用して管理すべき状態を減らせば、それだけロジックのコード量は減り、バグは減るでしょう。

まとめ

管理すべき状態減らすために、2つの方法を紹介しました。

  • ある状態について、他の状態から計算によって導くことができないか
  • ただキャッシュするだけなら状態として扱う必要はない

管理している状態を1つでも減らして、開発スピードを維持していきましょう。

最後に好きなセリフを。

『結果』だけだ!! この世には『結果』だけが残る!!

ありがとうございました。

脚注
  1. セレクターの呼び方は、ライブラリーによってはcomputed、あるいはget、getterかもしれません。 ↩︎

Discussion