ReactのSignalとAtomic State Managementの動向を追う
導入
最近のReactの状態管理ライブラリがスケールした先はstore自体をVanilla化して脱Reactする傾向にある。
(Zustand, Jotai, tanstack query, ...etc) [1][2]
理由として、Reactのコンテキストでstoreを持つと以下のような問題がある。
- useMemo, useCallbackなど本質的にノイズなコードが必要
- Reactのimmutableな制約の影響を受ける
storeをVanillaにすると
- storeに対するget, setを自然に表現できる
- インスタンスが変わらない(参照が同じ)なのでレンダリングのチューニングの余地がある
そして火付け役としてuseSyncExternalStoreや、use
フックがReact公式から提供された。
これを利用するとVanillaでもReact18以降のセマンティクスに対応してtransition, concurrentのメリットを享受できる。
もはやVanillaでstoreを持つことのデメリットは消えつつある。
以前と比較してstoreとReactを連携するためのルールやコードは遥かに減っている。
Vanilla化するとコードベース全体もシンプルになっていく。
というのも、プログラムの複雑性はドメインモデルと技術基盤の乖離の係数(いわゆるインタフェースを合わせるためのボイラープレート)によって増すという解釈(筆者独自)からすると、storeの基盤をVanillaにすると技術基盤の乖離が減り、ドメインモデルをより純粋かつ自然に表現できるようになるからだ。
Reactにおけるドメインモデルと技術基盤の乖離についての具体例は https://zenn.dev/link/comments/238de1fb4a497d に詳細を記している。
React Hooksでドメインモデルを表現することには限界がある。
界隈として、今まで本当はstoreをVanillaにしたかったが、Reactの制約によって難しかったのだろう。
それがReactの進化によって可能となった、とも言える。
useSyncExternalStore, transition, concurrentなど、ReactのDXとUXを両立させる進化はめざましい。
そのような背景から状態管理を脱Reactする流れが生まれてきたのだと思う。
それが結果的に設計をシンプルにし、Islands ArchitectureのようなReact以外のエコシステムを取り込んで選択肢を増やすきっかけにもなる。
論点
Vanilla化したstoreに対しreactivityをどう実現するかが論点となるが、大まかに2パターン存在する
- observer, proxyを組み合わせる (ここでは便宜上signalパターンと呼ぶ)
- ReactのuseSyncExternalStoreを使うパターン
本スクラップでは前者のsignalパターンの動向を追う。
なるべく中立的に各ライブラリの思想とトレードオフを挙げていき、signalの理解を深める。
また、個人的にAtomic State Managementを選定することが多いため比較対象としてRecoil, Jotaiを挙げている。
登場人物
Atomic State Management
Signal
-
Valtio
- 厳密にはproxyだが思想は似ているので一応
- jotai-signal
- legend-state
- preactのsignal
関連するReactのコア機能
-
パラダイムシフトの可能性を秘めているモノたち
-
- まだ研究中
- ReactのコンパイラでuseMemoを動的に作るやつ
-
- promiseを第一級オブジェクトとして扱う
- 将来的にはcache apiという形でstoreを持つ予定
-
Support Promise as a renderable node
- promiseをレンダリング可能なノードとして扱う
-
Form系ライブラリはあまりVanilla化されてないが、これはフォーカス管理などでrefが必要になることに起因している?作ろうと思ったら作れそうだが、複雑なForm State管理の需要は低いのか? ↩︎
-
この領域の先駆者は dai_shi 氏と Tanner Linsley 氏だと思う。https://twitter.com/dai_shi/status/1434543349524877317, https://twitter.com/tannerlinsley/status/1504907291291623427 ↩︎
jotai-signal
- https://twitter.com/dai_shi/status/1569542040010260482
- signalは実行時に値ではなくjsxに変換される
- 内部的にcreateElementすることで、コンポーネントをレンダリング対象としない
- https://github.com/jotai-labs/jotai-signal/blob/44be0f5acba15a85f9bbb3656cd8cadffabfe6b6/src/jsx-runtime.ts#L5-L11
legend-state
-
- Bravelyで2015年から2020年まで内部的に使われていた技術基盤をライブラリ化したもの
- 歴史が長いのでドキュメンテーションが充実してる
-
雰囲気的にはobserver, proxyを組み合わせたsignalぽい?
-
これもjotai-signalと同様、実行時にjsxに変換してる気配がある
- https://github.com/LegendApp/legend-state/blob/5b849ddcc359f54cbc18b9821a067f33e3b700ac/src/react/enableLegendStateReact.ts#L19-L23
- ソースコードに
Inspired by Preact Signals
のコメントが入ってた
-
observableは関数も持てるので、Zustandっぽくもできる
まだ調べきれてないこと
- Suspense対応
- testing
所感
- 第一印象としてはRecoil, Jotaiよりも柔軟性が高くて抽象化の筋が良いと思った
-
batchingとかonChangeとかcomputedとか
- RecoilのatomEffect, JotaiのonMount的な挙動を自然に書ける
- persistanceもbuilt in
-
batchingとかonChangeとかcomputedとか
- アプリの設計の文脈でいうと、スケールに合わせてreducerやaction的な概念でステートをひとまとめに更新したくなるので、そのあたりの知識は普遍的に必要
- 最初にポリシー定めておくのは大事
- いつでもどこでもmutableに変更しちゃうと後から破綻する
- MVVM時代に後戻りしないように
- コンポーネントを横断する際は適切にlifting state upするとよさそう
- 最初にポリシー定めておくのは大事
Reactのコアチーム(useRFC書いていた)人は、signalはUIコードの書き方としてはあまり良くないと思っているらしい。
We might add a signals-like primitive to React but I don’t think it’s a great way to write UI code. It’s great for performance. But I prefer React’s model where you pretend the whole thing is recreated every time. Our plan is to use a compiler to achieve comparable performance.
When we do add a signals-like primitive, it’ll mostly be geared toward serving as a compiler target, or as a low level API for library authors
仮にReactにsignalのようなリアクティブプリミティブを追加したとしてもそれは内部に隠蔽するのだろう。
自分もUIのプログラミングのメンタルモデルでは毎回全部作り直したことにするReactのイミュータブルなメンタルモデルに共感しているので、この意見は賛成する。
signalがUIのレイヤに登場するとコンポーネントの状態を司る重要なデータ構造が露出してしまい秩序を設けるのが難しくなる。
コンポーネントのレンダリングとそのデータの依存グラフの構築を分離するAtomic State Managementを支持しているところにもこのような理由がある。
Solid.js開発者のRyan Carniatoによる記事