RecoilからJotaiに移行してみた:注意点と実装Tipsまとめ
こんにちは!
株式会社カンリーでカンリー福利厚生の開発チームに所属しているaoyyです。
カンリー福利厚生ではフロントエンドの状態管理ライブラリとしてRecoilを使用していましたが、最近Jotaiに移行しました。
折角なので移行時の諸々をまとめていこうと思います!
背景
Recoilは元々2023年4月からアップデートがなかったのですが、2025年1月に正式に開発停止となりました。そのため、Reactのバージョンアップに対応されない、今後バグが見つかった際に修正が入らないなど問題となる可能性がありました。(実際React19では正常に動作しません)
チーム内ではある程度容易に移行可能かつ今後も開発継続が見込まれるJotaiに早めに置き換えしたいという話があがっていました。
その中でも今回は初回ということもあり、影響範囲の少ない管理画面側の移行を行うこととなりました!
RecoilとJotai
RecoilとJotaiはどちらもReact用の状態管理ライブラリです。
RecoilはMeta(Facebook)が開発したライブラリで、atomとselectorで状態管理を行います。atomは一意のキーを使用して状態を保持し、selectorでatomの読み込みや書き込みを行います。
サンプルコード
// atom定義
const count = atom({
key: 'count',
default: 1
});
// Selector
const doubleCount = selector({
key: 'doubleCount',
get: ({get}) => {
const count = get(count); // atomから値を取得
return state = count * 2;
}
set: ({set}, value) => {
// atomの値を更新
set(count, value);
}
});
JotaiはRecoilと違ってatomのみで状態の保持と読み込み、書き込みを行います。atomでの状態管理で一意のキーも使用しません。
サンプルコード
// atom定義
const countAtom = atom(5);
// atomのみで読み込み、書き込み可能
const newCountAtom = atom(
(get) => get(countAtom),
(get, set, _arg) => set(countAtom, 1)
);
移行時の注意点
移行にあたって、下記の観点で注意が必要です。
非同期状態管理
Jotaiでは Recoilのような loadable()
や selectorFamily
のような非同期処理の自動ハンドリング機構がなく、Suspenseを明示的に使う必要があり、下記のように非同期定義およびSuspenseの明示的な記述が必要です。
そのため、エラーハンドリングの制御も手動で追加してあげないといけません。
const atom = atom(async () => {
const response = await fetch('/api/asynchronously');
return response.json();
});
再レンダリング
Jotaiでは useAtom()だと読み込み+書き込みどちらのhookも取得してしまい、不要なレンダリングが起きる可能性があります。
そのため、useAtomValue()およびuseSetAtom()で分離するのが望ましいです。
型推論
初期値で型が決まるため初期値には適切な値の設定もしくは型の明示が必要です。
また、下記のように読み込み/書き込み可能で型が違うので型明示の際は注意して下さい。
- 読み取り専用...Atom<T>
- 書き込み可能...WritableAtom<Value, Update[], Result>
実際の移行作業
Jotaiのインストール
npm install jotai
Recoilのアンインストール
npm uninstall recoil --save
下記で置き換え
RecoilRoot
- <RecoilRoot>
+ <Provider>
<SWRConfig value={swrGlobalConfig}>
<Component {...pageProps} />
</SWRConfig>
- </RecoilRoot>
+. </Provider>
atom定義の書き換え
jotaiではキーでの識別を行わないので、キーなしでの定義に変更。
- export const stateAtom = atom<string>({
- key: 'state',
- default: '',
- });
+ export const stateAtom = atom('');
useResetRecoilState
下記のようにatomWithResetでのatom定義と、useResetAtomでの初期値へのリセット処理に修正しました。
// リセット可能なatomを定義
const countAtom = atomWithReset<int>(0);
// リセットするためのhookを取得
const resetCount = useResetAtom(countAtom);
// 初期値にリセット
resetCount();
useSetRecoilState
useSetAtomに置き換え。こちらは書き込み専用。
const stateAtom = atom("test")
- const setState = useSetRecoilState(stateAtom);
+ const setState = useSetAtom("dummy");
useRecoilValue
useAtomValueに置き換え。こちらは読み込み専用。
const stateAtom = atom("test")
- const setState = useRecoilValue(stateAtom);
+ const setState = useAtomValue(stateAtom);
最後に
Recoil→Jotaiの置き換えを行う際は移行時の注意点
にも記載したように、いくつか注意すべきところがあります。
管理画面の移行後はより影響範囲の大きいフロント側も移行が必要なため、少しずつ置き換えを進めていければと思います。
もし誤記載などありましたらコメントでご指摘いただけますと幸いです!

株式会社カンリーは「店舗経営を支える世界的なインフラを創る」をミッションに、店舗アカウントの一括管理・分析SaaS「カンリー店舗集客」の開発・提供、他複数のサービスを提供しております。 技術系以外のnoteはこちらから note.com/canly
Discussion
ぱっと目についたのでコメントです。
Recoilとの機能差は知らないのですが、loadableという関数自体はあります。