👻

RecoilからJotaiの移行にjotai-recoil-adapter便利でした

2024/09/16に公開

まとめ

RecoilからJotaiに段階的に置き換えるのにjotai-recoil-adapterは便利でしたよという話

また、使った際に不具合があったため、そのリマインドもしておきます。

前提

  • クライアントサイドの状態管理にrecoilを使っていた
  • recoilはすでにメンテナンスされていない
  • recoil起因でメモリリークが発生していた
    • atomに大きいオブジェクトを繰り返しsetしていたところ、古い値がGCされず残り続け、メモリを大量消費していた

そもそもrecoilがメンテされてないため置き換えるべきでしたが、結果としてメモリリークの問題も解消され良かったです。

置き換え方法の判断と選定

すべてのRecoilの記述を一気にJotaiに置き換えるのは困難(面倒)だったため、移行をサポートするライブラリを探しました。

  • jotai-recoil
    jotaijsから提供されていまうs。recoilのatomをjotaiに持ち込むもの
  • jotai-recoil-adapter
    jotaiの上にrecoilと互換なapiを構築したもの

段階的移行を考えるなら前者も候補ですが、今回は主目的がメモリリークの解消のため、後者を試しました。
いくつかオプション周りで小さい修正をしたもののほとんどそのまま動き、メモリリークの問題も回避できました。

jotaiとjotai-recoil-adapterの併用

併用の際の不具合

実は、jotai-recoil-adapterはjotaiと併用するする際に不具合があります。
jotai-recoil-adapterで作ったatomやselectorを、jotai側から正しく参照できません。

import { atom as recoilAtom } from 'jotai-recoil-adapter'
import { atom as jotaiAtom } from 'jotai'

const anAtom = recoilAtom({ key: "hoge", default: 3 })

const derivedAtom = jotaiAtom( get => get(anAtom) )
// ↑型は問題ないが、anAtomの値の更新がこちらに反映されない

原因

原因は、jotai-recoil-adapterのビルドに関する設定にありました。
jotai-recoil-adapterはビルドの際にdepsをbundleしているため、依存であるjotaiの中身もbundleされてしまいます。
このため、内部のgetDefaultStoreもbundleされ、外側のJotaiのstoreとは別のstoreで動作します。結果として、Jotaiのatomを参照しようとすると、異なるstoreで動作してしまい、期待通りに動作しないのです。

(期待通りに動かないのに型は間違っておらず、値をsetしたのに反映されない、という壊れ方だったので問題特定に時間を要して大変でした)

おそらく、ライブラリ開発者は、まるごと置き換えるケースを想定していて、jotaiと併用するのはユースケースとして想定していなかったのかもしれないです。
(一応issueを上げて聞いてみているのですが、返事がないので現在は特に使っていないのかもしれない)

対応

要するにビルド時にjotaiをexternalに指定すればよいだけですね。
元のリポジトリがアクティブではなかったので、一旦forkして自分で修正してpublishしました。

original:
https://github.com/clockelliptic/jotai-recoil-adapter/blob/ad3793616eb1b8b9161176d2d4a21a06f603667c/vite.config.ts#L24
fixed:
https://github.com/miyamonz/jotai-recoil-adapter/blob/95da4c7b7700b49edd57c14c43a0d8a81e9468da/vite.config.ts#L24

https://www.npmjs.com/package/@miyamonz/jotai-recoil-adapter

おわり

Recoil使ってたけど置き換えないとなあ…と思っていた方は試してみてはいかがでしょう?

Discussion