Open41

Atomic 系 State 管理ライブラリ上で Server State 管理を実現する試行錯誤メモ

ピン留めされたアイテム

https://zenn.dev/occar421/articles/why-data-fetching-library-for-spa#新しく生まれる問題や議論

拙著の「新しく生まれる問題や議論」において、Recoil を基盤としたデータ取得ライブラリがこれら問題を緩和するのではないかと述べた。このスクラップではその実現の試行錯誤を記録に残す。

Atomic という括りは下の記事で使っている分類。

https://blog.logrocket.com/jotai-vs-recoil-what-are-the-differences/

このスクラップでは Recoil も Jotai も使ってみる予定だが、前者の方が機能的には充実しているので、まず先に Recoil で試す。

Jotai は Context を使っているはずなので、Jotai のほうがミニマルで、Global State 管理基盤としてはきれいになると思う。

データ取得ライブラリと Global State 管理基盤

https://zenn.dev/akfm/articles/react-state-scope

Global State 管理手法の分断


State 階層の図示


分断

Global State 管理ライブラリを基盤に

https://zenn.dev/akfm/articles/react-state-scope#state分類とライブラリ-1

Server も Global State 配下の一ライフタイム分類としてこう配置したい。


Global State 再編


Recoil と Atom Effect による「プラグイン」

そこで Server 部分を解決する、Recoil の「データ取得プラグイン(仮称 recoil-cache)」が出てくる。このスクラップで解決策を探りたい箇所。

本当は Local State も統合したいのだが…。UI State という分類があるように。できれば Global State のほうが歩み寄る形で。

Jotai を使って "Context-ed" prefix を分類名に付ける形が良いのだろうか。


理想?


Jotai の活用例

Recoil は Recoil でまだ分断ができるような…(Recoil の領域が広いがこれは上の図との比較なのでここまでの分断はないはず)


Recoil の活用例

求められるインターフェース

あくまで単なる Recoil State として使えるようにしたい。

完全に TanStack Query や SWR のようなインターフェースにしてしまうと、その値を他の Recoil selector から利用できなくなってしまう。バンドルサイズの節約にはなるものの、わざわざ Recoil を裏にした Yet Another なライブラリを作る意義が薄れてしまう。

https://zenn.dev/link/comments/66f1ffd13dd5bb

透過的にしたい。例えば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 が勝手にやってくれる。

おそらく、利用者はデータ取得ライブラリよりも中間層を設ける必要が出てくるだろう。

最初は Recoil で、と思ったが、PoC としては Jotai の TanStack Query 統合機能に一工夫して↑を実現してみれば良さそう。

https://jotai.org/docs/integrations/query

ひとまずは利用者が直接 TanStack Query を触らない、意識しないところまでラップできればと思う。(それができれば今回の取り組みはもういったん終了でいいかもしれない。)

ひとまず queryKey は消し去る。

Jotai と TanStack Query の統合はあくまで統合であって、 TanStack Query の存在は意識させる。

atomWith??? とあるのは Atom Effect みたいに分離できると API 数が減るしモジュラになって良いのかもしれない。

↑のデモ。


動画コンバーターがやらかして最後の取得表示(灰色になった後)が切れている…

transition で切り替え可能にした。楽観的更新で transition 的動きが不要ということがあるのかはまた考えないといけない。

Mutation は、依存する Cache を常に invalidate する処理、コンポーネント毎にやりたい処理(invalidate に限らない)の両方を実行する。(useMutation を書くときには両方が混ざる。)

前者は下のスレッドで探索する。後者はここのスレッドに引き続き記載する。

Mutation の取り込み中状態は、Suspense で表現するのか否か 🤔

一応、(つまらないが)Custom Hook で対応することはできる。CUPID の Compasable や Unix philosophy に反する気もするが。

https://github.com/occar421/my-etudes/blob/61cfbd0e605ff0b59c2f35ce5dec654804263e5e/jotai-cache-sandbox/src/jotai-cache-poc/index.ts#L34-L54

(↑ status をマージする必要性に気付く前のコードなので分離してしまっている)

https://github.com/occar421/my-etudes/blob/61cfbd0e605ff0b59c2f35ce5dec654804263e5e/jotai-cache-sandbox/src/GetTime.tsx#L23-L36

Jotai の TanStack Query 連携機能が充実した。

https://github.com/pmndrs/jotai/releases/tag/v1.9.0

基本的に、本スクラップにおいて(ライブラリユーザーから見える部分で)試行錯誤することはほとんどなくなった。

Jotai においては、queryKey の存在を意識させないような制限ラッパーを用意するくらいになってしまった。

https://github.com/occar421/my-etudes/blob/f10b7ea96ae153874885f7297b222da8b8e00b6e/jotai-cache-sandbox/src/jotai-cache-poc/index.ts#L8-L55

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 をそのまま使うことだろう。

処理的に重複があり非効率そうではある。このスクラップでやりたいことを考えると元も子もないが。

そもそも論として、use が React に載ったら Jotai も Recoil も TanStack Query もかなり様変わりするし、本 Scrap でやろうとしていることは(意図してか意図せずかに関わらず)実現されるはず。

https://zenn.dev/uhyo/articles/react-use-rfc

use が実現するのであれば、これ以上今のタイミングで探求しても、研究としては意味をなさない気はする。

ログインするとコメントできます