Open41

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

ピン留めされたアイテム
MasuqaTMasuqaT

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

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

MasuqaTMasuqaT

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

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

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

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

MasuqaTMasuqaT

データ取得ライブラリと 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)」が出てくる。このスクラップで解決策を探りたい箇所。

MasuqaTMasuqaT

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

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


理想?


Jotai の活用例

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


Recoil の活用例

MasuqaTMasuqaT

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

あくまで単なる 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 みたいなクッションを置けるようにもしたい。

MasuqaTMasuqaT

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(/* ??? */);
MasuqaTMasuqaT

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 を借用すればいいかも。

MasuqaTMasuqaT

prefetch

queryClient.prefetchQuery(["foo"], fetchFoo);

sthPrefetch(fooState);

React コンポーネント外でどう実現するかは課題かもしれない。snapshot_UNSTABLE でいけるのだろうか。それか、Router の load に渡す関数を (SomeCallbackWithRecoilAccessor) => () => void にするんだろうか。

MasuqaTMasuqaT

Suspense 対応

useRecoilXXX を使うと Suspense モードに、useRecoilXXXLoadable を使うと非 Suspense モードになる。これはデータ取得プラグイン提供者は何も考えなくてよくて、Recoil が勝手にやってくれる。

SWR 的な動作も、Transition してもらえばよい。利用者に選んでもらう。こちらも Recoil が勝手にやってくれる。

MasuqaTMasuqaT

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

MasuqaTMasuqaT

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

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

MasuqaTMasuqaT

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

ひとまず queryKey は消し去る。

MasuqaTMasuqaT

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

MasuqaTMasuqaT

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

MasuqaTMasuqaT

↑のデモ。


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

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

MasuqaTMasuqaT

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

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

MasuqaTMasuqaT
MasuqaTMasuqaT

一応、(つまらないが)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

MasuqaTMasuqaT

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

MasuqaTMasuqaT

Mutation と Query Cache の間の依存管理について

Query 側で mutation atom を get していればすぐにできるが、基本は Mutation 側から更新先の Query を指定したい。

MasuqaTMasuqaT

やりたいことはこんな感じ。Atom に依存

const locationMutationCoreAtom = atomWithMutation(updateLocation);
const locationMutationAtom = atom(get => get(locationMutationCoreAtom), (_, set, args) => {
  set(locationMutationCoreAtom, args);
  set(timeAtom, { type: "refetch" }); // Query cache の invalidate
});
MasuqaTMasuqaT

Mutation の場面によって invalidate したい cache を変えたい気がしてきた。 A を mutate するときに「同時に」かつ「必ず」 B を invalidate したいという要求は実は無いのでは?

atom 部分にだけたくさん持たせるのは柔軟性が落ちて良くないのかもしれない。React コンポーネント内や Hook 内に invalidate を愚直に書くと良さそう。

MasuqaTMasuqaT

今できる現実的な構成としては、Jotai を使う場所での ServerState の参照系では jotai/query を使い、更新系では TanStack Query の useMutation をそのまま使うことだろう。

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

MasuqaTMasuqaT

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

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

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