Redux ToolkitでつくったSliceをJotaiで使う

2022/07/24に公開

ReactのConcurrent Renderのこと思うと外部で状態を保持するより、React上に保持させるほうがおそらく優位だろう(未検証)。仮説としては、状態の分岐が起きたときに外部の状態も分岐する必要があるからだ。
React上に保持できれば扱いやすくなるかもしれない。

では、Reduxでつくった状態はどうするのがよいだろうか。細かい方法はいくつか浮かぶがざっくりいうと以下の3パターンだろう。

  1. もしかするとreact-reduxがそういう実装になるかもしれないので、待ってみる
  2. Storeを外部サービスと捉え、React内にコピーを取る
  3. reducerを流用してuseReducerを使う

2についてはJotaiがReduxのadapterを持っている。store自体をatom化してしまうことができる。これがほぼ同義であるが、自分でつくるとしたらdispatchされるたびにsnapshotをとればいいだろう。
https://jotai.org/docs/integrations/redux

3については、useReducerにreducerを渡してしまえばそのまま使える。利用範囲に合わせて配置する方法が
考えられるだろう。
3の別解として、React自身の状態管理を用いるRecoil,Jotaiを使う中で流用してしまう手がある。

流用方法としては、Recoilは性質上カスタムフックでラップするような形になりそうで、Jotaiの場合はAtomにすることができそうである。

ということで、 試しにRedux ToolkitのcreateSliceで作ったSliceをJotaiで使ってみることにする。
Redux ToolkitでかいたアプリケーションをJotaiへ書き換える記事があったので、こちらと同じ題材を活用してみる

ということで以下の記事を参考にしています。
https://zenn.dev/tell_y/articles/9a2339eb7e83cf

Redux Toolkitで書かれたコードがここにあるので、ここからforkeします。
https://codesandbox.io/s/github/reduxjs/redux-essentials-counter-example/tree/master/?from-embed=&file=/src/App.js:0-27

そして、こちらがcreateSliceで作ったsliceをJotaiを使って利用している例だ。
https://codesandbox.io/s/create-slice-with-jotai-yi06dq?file=/src/features/counter/Counter.js

なお、本題とは無関係の修正として、利用ライブラリを最新に更新しています。

使い勝手

sliceToAtomという値を保持しているatomとReduxのActionを渡すと、dispatchと同じインターフェースで利用できる書き込み用の算出atomを生成するヘルパを用意しました。

export const [counterAtom, dispatchCouterAtom] = sliteToAtom(counterSlice);

これを使う場面では以下のように修正します。同じ変数へ束縛し、かつinterfaceも同じなので後はそのまま使えます。
(型推論がちゃんとできるかは試していない)

+const count = useAtomValue(counterAtom);
+const dispatch = useSetAtom(dispatchCouterAtom);
-const count = useSelector(selectCount);
-const dispatch = useDispatch();

実装

// reducerと値を保持するatomから算出atomを作るときに渡すwrite関数を生成する関数をつくる
const createSyncAtomWriter = (reducer, atom) => (get, set, action) => {
  return set(atom, reducer(get(atom), action));
};

// 上記で作ったwriterはthunkに対応していないのでthunkも処理可能にする
const transAtomThunkWriter = (write) => (get, set, action) => {
  if (typeof action === "function") {
    action((afterAction) => write(get, set, afterAction));
  } else {
    write(get, set, action);
  }
};

// 初期値とreducerから、値を保持するatomとdispatch用の算出atomを作る
const reducerToAtom = (initialState, reducer) => {
  const baseAtom = atom(initialState);
  const write = createSyncAtomWriter(reducer, baseAtom);
  const thunkWrite = transAtomThunkWriter(write);
  const dispatchAtom = atom(null, thunkWrite);
  return [baseAtom, dispatchAtom];
};

// sliceから初期値をreducerを取り出して、値を保持するatomとdispatch用の算出atomを作る
export const sliteToAtom = (slice) => {
  return reducerToAtom(slice.getInitialState(), slice.reducer);
};

まとめ

reducer自体がプログラミングにおいて非常に汎用性の高い部品である。
そのため、Jotaiへのアダプターを簡単に作ることができた。

ReactのuseStateもuseReduxを用いて作られていることを考えるとなんの不思議もないだろう。
reducer自体がgetやsetを引数や戻り値によって共通処理としてStoreにまかせているだけであるため、この部分を書いて上げればよいのである。

具体的には以下の部分である。

(get, set, action) => set(atom, reducer(get(atom), action))

atomから値を取り出し、引数で受け取った値とともにreducerに渡して、結果をatomに戻すだけなのである。
そんなわけでredudcerは無駄になりにくいので、状態処理の書き方に迷ったときの選択肢として優秀かもしれません。

Discussion