Atomic 系 State 管理ライブラリ上で Server State 管理を実現する試行錯誤メモ
拙著の「新しく生まれる問題や議論」において、Recoil を基盤としたデータ取得ライブラリがこれら問題を緩和するのではないかと述べた。このスクラップではその実現の試行錯誤を記録に残す。
データ取得ライブラリと Global State 管理基盤
Global State 管理手法の分断
State 階層の図示
分断
Global State 管理ライブラリを基盤に
Server も Global State 配下の一ライフタイム分類としてこう配置したい。
Global State 再編
Recoil と Atom Effect による「プラグイン」
そこで Server 部分を解決する、Recoil の「データ取得プラグイン(仮称 recoil-cache)」が出てくる。このスクラップで解決策を探りたい箇所。
求められるインターフェース
あくまで単なる Recoil State として使えるようにしたい。
完全に TanStack Query や SWR のようなインターフェースにしてしまうと、その値を他の Recoil selector から利用できなくなってしまう。バンドルサイズの節約にはなるものの、わざわざ Recoil を裏にした Yet Another なライブラリを作る意義が薄れてしまう。
透過的にしたい。例えばAPI に繋ぎこむまではモックデータをただ返す Atom に切り替えたい。Effect 付き Atom とか Selector にもしたい。直接 Server State につながず、楽観的 UI 用の State みたいなクッションを置けるようにもしたい。
query
const _foo = useQuery(["foo"], fetchFoo);
const foo = useRecoilValue(fooState);
const derivedFoo = useRecoilValue(derivedFooState);
const [optimisticFoo, setOptimisticFoo] = useRecoilState(optimisticFooState);
const fooState = sthServerState(/* ??? with `fetchFoo` */);
const derivedFooState = selector({
key: "derivedState",
get: ({ get }) => ({ ...get(fooState), ...get(barState) })
});
const optimisticFooState = sthWrapper(/* ??? */);
mutation
謎
const { mutate: _addFoo } = useMutation((args) => postFoo(args), {
onMutate: () => {}, // 何かしらの楽観的更新の実装
onSettled: () => { queryClient.invalidateQueries(["foo"]); },
});
const addFoo = useSthMutation((args) => postFoo(args), {
onMutate: (args) => { setOptimisticFoo(args); },
onSettled: () => { resetFooState(); },
});
そのまま useMutation
を借用すればいいかも。
prefetch
queryClient.prefetchQuery(["foo"], fetchFoo);
sthPrefetch(fooState);
React コンポーネント外でどう実現するかは課題かもしれない。snapshot_UNSTABLE
でいけるのだろうか。それか、Router の load
に渡す関数を (SomeCallbackWithRecoilAccessor) => () => void
にするんだろうか。
Suspense 対応
useRecoilXXX
を使うと Suspense モードに、useRecoilXXXLoadable
を使うと非 Suspense モードになる。これはデータ取得プラグイン提供者は何も考えなくてよくて、Recoil が勝手にやってくれる。
SWR 的な動作も、Transition してもらえばよい。利用者に選んでもらう。こちらも Recoil が勝手にやってくれる。
おそらく、利用者はデータ取得ライブラリよりも中間層を設ける必要が出てくるだろう。
mutation はこの機能の行く末をウォッチすれば良いインターフェースを考えられそう。
urql 統合の mutation はこんな感じでシンプル
最初は Recoil で、と思ったが、PoC としては Jotai の TanStack Query 統合機能に一工夫して↑を実現してみれば良さそう。
ひとまずは利用者が直接 TanStack Query を触らない、意識しないところまでラップできればと思う。(それができれば今回の取り組みはもういったん終了でいいかもしれない。)
ひとまず queryKey は消し去る。
atomFamilyWithQuery
はまだない
こういうのもある
Jotai と TanStack Query の統合はあくまで統合であって、 TanStack Query の存在は意識させる。
開始。これはベーシックな jotai/query 。
atomWith???
とあるのは Atom Effect みたいに分離できると API 数が減るしモジュラになって良いのかもしれない。
useMutation
は TanStack Query からそのまま拝借して、こんな感じに。
楽観的 UI 用の状態を検討するうえでどうやって Atom を watch すればいいのかと思ったが、unstable_createStore
という方法があった。
チート(global に定義した unstable_createStore
)は使ったが楽観的更新の Atom も完成した。
↑のデモ。
動画コンバーターがやらかして最後の取得表示(灰色になった後)が切れている…
transition で切り替え可能にした。楽観的更新で transition 的動きが不要ということがあるのかはまた考えないといけない。
Mutation は、依存する Cache を常に invalidate する処理、コンポーネント毎にやりたい処理(invalidate に限らない)の両方を実行する。(useMutation
を書くときには両方が混ざる。)
前者は下のスレッドで探索する。後者はここのスレッドに引き続き記載する。
Mutation の取り込み中状態は、Suspense で表現するのか否か 🤔
一応、コンポーネント内に onMutate
や onSettle
相当の処理を書けば事足りるが、取り込み中状態は全部手動で書く羽目になる。
Mutation のために、Atom を生成する Hook を作って分離してみたが… 使い勝手は微妙だ。(Atom 内で更新しているから?か Transition も効かない。)
Jotai Suspense が来れば、Transition に失敗するのは回避できるかもしれない。
一応、(つまらないが)Custom Hook で対応することはできる。CUPID の Compasable や Unix philosophy に反する気もするが。
(↑ status をマージする必要性に気付く前のコードなので分離してしまっている)
Jotai の TanStack Query 連携機能が充実した。
基本的に、本スクラップにおいて(ライブラリユーザーから見える部分で)試行錯誤することはほとんどなくなった。
Jotai においては、queryKey
の存在を意識させないような制限ラッパーを用意するくらいになってしまった。
Mutation と Query Cache の間の依存管理について
Query 側で mutation atom を get していればすぐにできるが、基本は Mutation 側から更新先の Query を指定したい。
やりたいことはこんな感じ。Atom に依存
const locationMutationCoreAtom = atomWithMutation(updateLocation);
const locationMutationAtom = atom(get => get(locationMutationCoreAtom), (_, set, args) => {
set(locationMutationCoreAtom, args);
set(timeAtom, { type: "refetch" }); // Query cache の invalidate
});
Mutation の場面によって invalidate したい cache を変えたい気がしてきた。 A を mutate するときに「同時に」かつ「必ず」 B を invalidate したいという要求は実は無いのでは?
atom 部分にだけたくさん持たせるのは柔軟性が落ちて良くないのかもしれない。React コンポーネント内や Hook 内に invalidate を愚直に書くと良さそう。
例えばこんな感じ
今できる現実的な構成としては、Jotai を使う場所での ServerState の参照系では jotai/query を使い、更新系では TanStack Query の useMutation
をそのまま使うことだろう。
処理的に重複があり非効率そうではある。このスクラップでやりたいことを考えると元も子もないが。
Jotai において、(連携部分が別パッケージに切り出されたことで) TanStack Query 機能が充実した。
もうこれを使えばいいんじゃないですかね…
そもそも論として、use
が React に載ったら Jotai も Recoil も TanStack Query もかなり様変わりするし、本 Scrap でやろうとしていることは(意図してか意図せずかに関わらず)実現されるはず。
use
が実現するのであれば、これ以上今のタイミングで探求しても、研究としては意味をなさない気はする。
同じ時期に同じ目的を持ったものとして拝見させていただきました。
道中のアプローチは違うながらも、わたしも結論は同じでした。
現在ServerState管理は過渡期にあるのでuse
RFCとJotaiの今後を見守っていくことにします。