STERFIELD

Reactの状態管理ライプラリはRecoilとJotaiならどちらをえらべばいいか?

Reactの状態管理ライプラリはRecoilとJotaiならどちらをえらべばいいか?

はじめに

Reactでフロントエンドの開発を行う場合に、おそらく一度は誰でも状態管理に頭を悩ませるのではないでしょうか。
2023年10月現在、Redux、React Hooks、zustand、そしてここで取り上げる Recoil、Jotai など、Reactの状態管理をするためのライブラリや方法は数多く存在ます。
多くの開発者によりその使い方が模索・洗練されていますが、絶対的な正解というものは存在しません。
故に、プロジェクトに導入する場合に、どれを選定するかで、とても迷う方は多いのではないでしょうか。
このブログでは、私が状態管理のライブラリを選定するときに悩んだ内容から、案件の向き不向きについて考察し、ご紹介したいと思います。

[2024.6.3 追記] Recoilから正式に発表されているわけではありませんが、2023年3月以降アップデートがなく、Recoilの開発は実質的に打ち切られてしまったようです。そのため、Recoilを選択することは避けた方がよさそうです…
参考: React 19 support · Issue #2318 · facebookexperimental/Recoil

→ [2024.7.19 追記] Reduxへ移行する方法の記事を投稿しました。

まず Redux、zustand と Recoil、Jotai の違いについて

Redux は、Reactを作ったFacebookが提唱したFluxというアーキテクチャをもとにした、初期から最も利用されている状態管理ライブラリです。
Reactとは切り離して1つのストアに集約して管理するため、情報の流れが一方通行で辿りやすい特徴があります。WEBアプリ全体の一貫性を重視し、堅牢な作りにする場合、Reduxは有望な選択肢となります。
しかしながら、Reduxは非同期通信を伴うような複雑な状態管理ではそのまま使うのはあまりにもコードが膨大になってしまうため、サードパーティのプラグインなどを導入することが現実的な選択肢となります。また、1つのストアで管理するため、コンポーネント単位でのカプセル化が苦手です。

zustand はこのReduxの苦手な部分を補うことで、Reduxからの乗り換えが進んでいるライブラリです。
基本的な思想はReduxと同じですが、Reduxで煩雑になるような記述をより簡潔に書くことができます。

Recoil は Redux とは違う思想で、ストアを複数持ち、それぞれに対して別々のコンポーネントからアクセス可能です。ストアが一元管理されているReduxやzustandとちがい、コンポーネント単位でのカプセル化がしやすく、データとアプリ全体の結合を制限することに向いているので、柔軟性を重視するアプリに向いていると言えそうです。 ただ、複数のストアに別々のコンポーネントから双方向でアクセスできる特徴が故に、組み方によってはReduxと比べると情報の流れを辿りにくくなる可能性があることに注意が必要です。

Jotai は Recoil をより簡潔に記述できるようにしたライブラリで、npm trendsを見ると、2023年10月現在、本家のRecoilを追い抜いています。

RecoilよりJotaiが向いている案件

RecoilとJotaiで同じ状態管理をするためのコードを比べてみましょう。

Recoil

1const exampleState = atom({ 2 key: "exampleState", 3 default: 0 4}); 5const exampleSelector = selector({ 6 key: "exampleSelector" 7 get ({ get }) => `${get(exampleState)}個を選択中` 8});

Jotai

1const exampleState = atom(0); 2const exampleSelector = atom((get) => `${get(exampleState)}個を選択中`);

こうやって並べてみるとよく分かりますが、
Jotaiの方が圧倒的にコード数が少なくなります。
Recoilで用意されている機能の多くはJotaiでも利用できるので、よりシンプルに状態管理を行いたい場合は、 Jotaiが向いていると言えます。

JotaiよりRecoilが向いている案件

では、次の場合はどうでしょう。

Recoil

1// Writable 2const idState = atom({ 3 key: "idState", 4 default: 1 5}); 6 7// Read Only 8const fetchState = selector({ 9 key: "fetchState", 10 get: async ({ get }) => { 11 const id = get(idState); 12 try { 13 const response = await fetch(`example.api.json?id=${ id }`); 14 const json = await response.json(); 15 return json; 16 } catch (error) { 17 throw error; 18 } 19 } 20}); 21 22// Writable (default value fetched) 23const complexState = atom({ 24 key: "complexState", 25 default: selector({ 26 key: "complexSelector", 27 get: async ({ get }) => { 28 const json = await get(fetchState); 29 return { 30 id: json.id, 31 name: json.exampleName 32 } 33 } 34 }) 35});

Jotai

1// Writable 2const idState = atom(1); 3 4// Read Only 5const fetchState = atom(async (get) => { 6 const id = get(idState); 7 try { 8 const response = await fetch(`example.api.json?id=${ id }`); 9 const json = await response.json(); 10 return json; 11 } catch (error) { 12 throw error; 13 } 14}); 15 16// Writable (default value fetched) 17const defaultComplexState = atom(async (get) => { 18 const json = await get(fetchState); 19 return { 20 id: json.id, 21 name: json.exampleName 22 } 23}); 24const overWrittenComplexState = atom(null as { id: number; name: stirng; }); 25const complexState = atom<number, [number], void>( 26 async (get) => 27 (await get(overWrittenComplexState)) || 28 (await get(defaultComplexState)), 29 (_get, set, update) => set(overWrittenComplexState, update) 30);

このように初期値をフェッチしつつ、後からデータの上書きもできるようなストアを作ろうとするような複雑な処理をさせようとした場合、JotaiよりRecoilの方が分かりやすいコードになることがわかるかと思います(もっと上手な書き方があったらすみません)。
また、フェッチしたデータをリセットできるようにする場合、

Recoil

1const exampleState = atom({ 2 key: "exmapleState", 3 default: 0 4}) 5const ExampleComponent = () => { 6 const [example, setExample] = useRecoilState(exampleState); 7 const reseter = useResetRecoilState(exampleState); 8 const handleAddClick = () => setExample(previousState => previousState + 1); 9 return ( 10 <div> 11 <p>{example}</p> 12 <button onClick={handleAddClick}>追加</button> 13 <button onClick={reseter}>リセット</button> 14 </div> 15 ) 16}

のように、Recoilはatomに対してそのままuseResetRecoil()を使うことでリセット可能ですが、

Jotai

1const exampleState = atomWithReset(0); 2const ExampleComponent = () => { 3 const [example, setExample] = useAtom(exampleState); 4 const reseter = useResetAtom(exampleState); 5 const handleAddClick = () => setExample(previousState => previousState + 1); 6 return ( 7 <div> 8 <p>{example}</p> 9 <button onClick={handleAddClick}>追加</button> 10 <button onClick={reseter}>リセット</button> 11 </div> 12 ) 13}

のように、Jotaiでは、あらかじめreset用のutil(atomWithRest)を使ってatomを用意する必要があります。
その他にも、Jotaiでは複雑な操作をしようとした場合に、Recoilと同様の操作をするためにより多くのutilを使ったり、helperを自前で用意しなければいけない場合に遭遇します。

これらのことから、より複雑なデータ処理を扱うような使い方をする場合、Recoilの方が向いていると言えます。

まとめ

堅牢でアプリ全体を見通すことを優先するのであれば Redux。
基本的な思想はReduxと同じだけど、よりシンプルに描きたいのであれば zustand。
機能追加やコンポーネントの入れ替えが多いような柔軟性のあるアプリで、複雑なデータ管理が必要なら Recoil。
柔軟性とシンプルさを重視するなら Jotai というのが、現時点での自分の選択基準でした。

参考リンク

Author Profile

著者近影

NINOMIYA

Webデザイナー兼コーダー出身のフロントエンド開発者です。 UXデザインやチーム開発の効率化など、勉強中です。

SHARE

合わせて読みたい