タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

ReactにとってStoreライブラリなんてもんはほんまにどうでもいい

Sponsored link

まとめ

  • なんでもいい、どうでもいいと言い出すやつはだいたい内心でどうでもよくないと考えている

  • Storeの話をするまえに https://react.dev/learn を読んでReactの基礎を理解しよう

  • experimental state management frameworkと一生言い続けているライブラリをプロダクションで使うべきではない

  • experimentalと一生言い続けているライブラリをプロダクションで使うならその部分は特に気をつけて疎結合にすべき

  • ライブラリの話をしてないでreact.dev/learnのThinking in Reactを読もう

  • とにかくreact.dev/learnを全部読んで、Reactのstateの基礎コンセプトを理解してからStoreへ移動しよう

  • useContextは似て非なる存在で擬似的な流用はできるがまともな代用たりえない

    • useStateも同様
  • なぜReactのhookだけでStore管理しよう論者の多くはuseReducerやuseSyncExternalStoreに言及しないのか

  • React世界からみてStoreライブラリの詳細とかどうでもいいので癒着の余地はない

    • よくない場合、アプリ実装に原因がある
  • ライブラリの批判をする前にreact.dev/learn LEARN REACTを読もう

  • Store的機構の流用自体は有用だが、覚悟をもってやるべき。あとそういう目的外利用の結果でライブラリに文句をいってはいけない

  • 初心者は、Reduxは使わないにしても Style Guide | Redux には一度目を通しておこう

    • お前がこれから踏む地雷の位置がだいたい書いてある
  • https://react.dev/learn は、やる気があるぶんだけ無料で読めちまうんだ!

Reactはフレームワークではなくあくまでライブラリ。
つまり実装者に設計の主導権がある。
自由は責任を伴う。

プライベートが忙しくなりすぎてあまりキャッチアップの時間がとれてなかったけど、どうもここ4〜5年くらいの間、だいぶフロントエンドは停滞してたみたいですね。
まっ、エッジ領域(ダブルミーニング)は進歩しすぎてわけわからないからバランスはとれてるんだけどね。

ただしuseContextテメーはだめだ。

以下でちょろっと説明しますが、useContextを仲間外れにしたのはselectorとisEqualが組み込めないからです。
パフォーマンスがほんの少しでも問題になった瞬間、抽象化に漏れが生じます。

「Storeに依存するのは嫌だからuseContextとuseStateで全部書くわwww」
という選択は、もちろんプロジェクト主導者の自由ですが明らかに欠如した機能を補うためのハックが必要になりプロジェクトの保守性が落ちる可能性があります。
またuseContextWithSelectorが本格的に仕様化された際にそれらのハックが陳腐化するリスクもあります。
あとuseStateはAppStateどころかFormStateの管理にも機能不足なのでuseReducerが検討されないのも疑問符です。

現状、ReactのContextはFeatureFlagのようなアプリケーション初期値の注入、あるいはカラーテーマやURL管理など、変更が全体の再レンダリングを示唆するような要素にのみ限定して使うものでしょう(諸説あり)。
久々にこの辺の議論を深めたくなってきた。

Storeライブラリ

Reactの世界からみて、Storeなんてマジでなんでもいいです。
ガチでどうでもいいとおもえない場合、それはReact ComponentがStoreにガッツリ依存しているということを指します。
それは設計の誤りです。

useSyncExternalStoreというAPIが設定された以上、Reactの世界にとってStoreは (onStoreChange: () => void) => () => void の関数を提供してもらい、更新を監視してViewのレンダリングに反映させるもの。
ただそれだけです。

つまり上記のsubscribe/unsubscribeと、(s: State) => T のselector、場合によっては (a: T, b: T) => boolean。必要なものはAPIはこれだけです。
(あるいはqueryとそのcacheを擬似的なStoreとして扱うアプローチも流行っていますが、筆者はこのハイリスクなアプローチをたいへん嫌っているので今回は言及しません)

アーキテクチャ的な階層を隔てた要素はinterfaceに対して依存するべきであり、実装に依存すべきではありません。
全てを依存させることで初期速度をブーストさせる選択肢(Ruby on Railsが代表例)もありますが、こういったおもいきったやり方は相応のリスクを伴います。
例えば、利用しているStoreライブラリが独特のもので、かつ保守されなくなった場合です。
ここに最適解はありません。

リスクとリターンを考えて各自が選択すべきですが、何も考えずにリスクをとって失敗したら「またフロントエンドは保守が云々」と泣き言をいうのは不健康です。
Reactはシンプルなので選択の自由と責任が生じます。
Reduxというコンセプトを実装でき、そして自分のアプリの要件に合うミニマル版を自作できる程度の知見はない限り中規模以上のアプリを保守性高く作るのは難しいです(Reduxが良いといっているのではなく、定番の状態管理について守破離できる知識があるかという話)。

Storeとは何か

Storeとはアーキテクチャ上の抽象概念であり、実装の詳細、あるいはその関数が使っているライブラリを指すものではありません。

例えば貴方のコンポーネントがURLから何かデータを引っ張って来て中身の出しわけをするとしましょう。
しかも何らかの理由があってそれにrouterライブラリを使わずやりたい。

そこでパラメーターの操作と同時にRecoilへ副作用を起こし、購読しているコンポーネントの中身が書き換わるように設計しました。

この場合、中で動いている実装の詳細をみればStoreですが、フロントエンドのアーキテクチャ的抽象度でいえばその役割はRoutingです。
故に、ここでRecoilがサ終した場合はこのhookを適当な仕組みで修正しなおせば終わりです。
数分で終わる作業です。

もしも……ここでReactコンポーネントの修正が要求された場合は実装に多大な欠陥があることを意味します。
ここの実装の抽象は
「URL、あるいはそれに準ずる機構にキャッシュされた情報とコンポーネントを同期させる」
わけで、つまりRecoilが消えようがどうしようが「URL、あるいはそれに準ずる機構にキャッシュされた情報」を意味する関数の中身を修正すればそれで終わりです。
そうならないのは実装のミス、設計のミスです。
意図を表明したCustom Hooksを作り、コンポーネントはそれを利用すべきです。Recoilの責任ではありません。

また「レンダリングのたびにComponentがURLを確認する」という修正アプローチも謎です。
それではURLの変更を購読できません。
この性能差分が許されるということは、やはりそもそも購読が必要ない要素にStoreを流用していたという根本的な実装ミスを示唆しています。

その他……日頃より色々な人たちが色々なStore管理ライブラリについてこれがいいとか、これがダメだとか言い合ってますがだいたいが自分の実装の欠陥を何かに押し付け、たまたまその欠陥を補ってくれるような無駄機能がついてるライブラリを褒めているだけのケースが多いです。確証バイアスですね。

機能が少ないライブラリはその分自分で補う必要がでてきますが、保守切れのリスクは減ります。
機能豊富なライブラリは公式サンプルのコピペでやれる範囲が広がりますが、依存症のリスクがあります。
Reactが強いてくるこの二者択一が嫌な人はVueを使って破壊的なバージョンアップのたびに対応のやり方についてブログを書けばいいとおもいます。

少なくともSelectorという概念、あるいはReactにおける「保存より計算」という基礎の基礎コンセプトが理解できていない人がRecoilを使って保守切れに文句をいうのは明らかに間違っています。
初期のgetterの中身がただのAPIなのもあれですが、そこで代替として出てくる「一つのオブジェクトに派生状態を保存」という解決法は、数年に一度、よほど解決困難なパフォーマンス問題が生じた時に特盛の言い訳コメントと共にコードレビューであれこれ検証されて、それでようやく実装が許されるかもしれないレベルのありえない実装です。
Recoilのせいにするのはやめましょう。

あとhttps://react.dev/learnを読みましょう。
忌憚の無い意見ってやつっス

Storeがどうでもよくなくなるとき

よくなくなくないときです。

かなりの大規模フロントエンド(関わる人は少ないでしょう)。
なおかつパフォーマンスをかなり詰めないといけない人。

この場合はStoreの比較が大事になる……かもしれません。
しかしまあWebサーバーにとってのIOと同じくらい、フロントエンドのボトルネックはレンダリングです。

大規模に耐える機構の有無(個人的にはSliceを上手に扱えないと厳しい)、あとはStoreへの副作用にPromiseやRXとの接続性を求めるかどうかくらいでしょうか。
古式のreducerは認めませんでしたが、最近は認めるものが多いように見受けられます。あとマジでObservable対応してくれ、自作のStoreライブラリでは対応させたけどほんとにこれ便利なんすよ。

Recoilというコンセプトについては……わざわざリスクをとるほどの革新性が感じられないので興味なかったです。誰か教えてくれ……
あとselectorがStoreと同質なのはいいとして、setterを持つ価値もあまりわからなかった。
Redux特有のクソデカJSONが不要になるのも、もしそれが問題ならばMulti StateなReduxを自作すればいいだけ(DevTool連携とかは捨て)。

Storeライブラリの詳細が明らかになるのは、状態管理ロジックの中だけです。
まさかとはおもいますが、Componentの中で状態管理をする人はいないとおもいます。

つまりAction, Feature, Module, Ducks, Reducer, Service……まぁなんでもいいですがこういったビジネスロジック層をいかに快適に書けるか?
という議論をする際に有用になります。
実際多くのStoreライブラリはVanillaなJSですら利用できます。そらそうだ。

現場からは以上。


余談。

チラッと述べたとおり、筆者はRecoilというコンセプト……が既存の状態管理をどう変えるのかあんまりよくわかりまへん。
selectorがsetter持てるとアプリ設計の何が変わるんや?
状態そのものと計算を区別しないで扱う発想自体はRedux時代からかわらんしなぁ……APIレベルで支援してくれてるってことなんだろか。

わかりやすく対立させると、同一作者のzustandとjotaiがあるとおもうけど……どっちでも大差なくない?
大差ないならzustandのほうがシンプルだからzustandかな。でもzustandですらちょっと親切すぎるかも。もっと機能少なくていい。
こういう枯れた思考になるのは歳をとったからかな。

ところで大昔に書いて放置してあるこのブログのCSSが崩壊しつつあるのでなんとかしたい。