いまさらになってReduxをいれるか悩んでる話

まとめ
実務で4年ぐらいReactを使った開発をしているがなんやかんやまだReduxを入れたことがない。
最近になってReact-reduxを入れようか悩んでいる話

Reduxを入れてない理由
作者のDan先生も言っていますが、必要にならなかったわけですね。
もっとReduxじゃないとキツい!ってなれば学んで入れようってモチベになるんですが案外なくてもいけちゃったなーというのが素直な感想。

Reduxが必要にならなかった理由
バックエンドがGraphQLで、GraphQLクライアントライブラリが優秀だったことが挙げられます。
- Apollo Client
- URQL
- React Query
上記3つを使い現在はReact-Queryを使っているが、fetchデータの状態管理はReact Queryのcacheで十分賄えている。
Form周りはReact-hook-formをずっと使っているので(要はReact-hook-formのスポンサーです)Form周りでも特に必要としなかった。
- 非同期処理での状態管理
- Formの状態管理
上記2点が自分が状態管理ライブラリに欲していた8割なのでなかなかReduxを入れるモチベが弱かったという理由。
残りの2割はuseStateとpropsバケツリレーでもどうにかなっているのはまだReduxを欲するほどフロントでの複雑なデータ管理アプリの開発規模ではないのかなとも考えていた。
あとは、hooks導入前にReduxを触っててHOCなRedux処理が悪い印象というのも否定できない。

Recoilを入れた理由
実はReduxではなくRecoilを導入していた。
uhyoさんの上記記事は大変参考になりました。
記事の流れと同じように
- useState + バケツリレーからはじめ、
- useContextの使用をしていました。
useContextに簡単なObjectを渡すことで面倒なバケツリレーは対処できましたが、だんだんuseContextのプロバイダーが複数必要になり記事通りの問題に直面し、ライブラリの導入を検討しました。
ここでの検討ポイントとしては上記で上げたように
- 非同期処理の状態管理
- Formの状態管理
の2点は必要なかったです。
必要なのはHeader等に表示するユーザーの状態だったり局所的に複数コンポーネントをまたぐようなデータだったりと割とuseContextでも耐えようと思えば耐えられる規模です。
そこを考慮するとRedux(react-redux)では重いのではないか?という懸念がありました。
特にuseStateでのモーダル開閉管理等はそのまま併用したいのでStateをStoreに集めるReduxはハードルが高かった(useStateとReduxの併用をしないべきという考えがあった)
そこでRecoilの導入をしてみました。
特に上記のアーキテクチャ上の特徴で説明されている図にもあるユースケースがドンピシャでした。
先ほどの図では右上にAtomがまとまっていましたが、次の図のように、コンポーネントツリーの末端でちょっとだけ使われる(しかし複数のコンポーネントで共有される)ようなユースケースにもRecoilは適しています。
なので、React QueryでGlobalで管理したいデータ等をRecoilを使って管理をはじめました。
いま振り返ると順調な状態管理ライブラリのステップアップをしていていい感じですね。

現状の問題点
Recoilを入れたのはいいが大きく問題が発生した。
React QueryのデータをRecoilに入れて管理すると2重管理になり、React QueryのCacheとRecoilでの有効期限の差が出て思わぬバグが発生しだした。
問題を整理すると下記の状態がGlobalで必要だった。
- React Queryで発行したQueryの状態
これをRecoilで管理するとReact Queryを利用したComponentはそのままQueryの状態を見れるが、他のComponentはRecoil経由になる。ここで有効期限の差で思わぬ動作をしてしまう。
React Queryはcodegenを利用してHooksを生成しているのだが、Queryの発行時のkeyを取得できるオプションが存在した。(ここのexposeQueryKeys)
このkeyを使えばQueryの状態は見れることがわかった。
現状は
- React Query: Fetchデータ管理
- Recoil: 複数コンポーネントをまたぐ状態管理とQueryの状態管理
- useState + useContext: UIでの簡単な状態管理
の3つを利用している。
React QueryとRecoilがややこしく絡んでいるのでここで整理しようといろいろ検討した。

検討1: React Queryで状態管理を統一してみよう
上記記事を見たり別のライブラリであるSWRでも似たような記事があった。
React Queryがメインなのでそれでローカルの状態管理を任せれれば楽だなと思い調査と実装をした。
import { useEffect } from "react";
import { useQuery } from "react-query";
import type { QueryKey } from "react-query";
export const useFetch = <T>(key: QueryKey, initial?: T) => {
const { data, status } = useQuery(
key,
async () => {
const res = await fetch("/api/demo");
return (await res.json()) as T;
},
{
initialData: initial,
}
);
return data;
};
こんな感じに書けばuseStateみたいな使い方ができた。
ただ、複数コンポーネントをまたぐような使い方ではなかった。
useStateの置き換えもする必要はない上になんかReact QueryのQuery状態に別の状態を混ぜるのは余計な管理を増やしそうなのでやめた。
独自な使い方をするほど大事なもんでもないかなとなった。
結論: 見送り

検討2: Recoilではなくjotaiを使ってみる
そもそもReduxは早いからRecoil入れたけどこれでも大きすぎるかな?!ってなった。
上記記事でのサイズ比較でRecoilってでかいんだなってなる。
必要なものは useState + useContextを便利に使いたいというものなので
コンセプトも比較もRecoilより簡潔で私のニーズは満たせそうと感じた。
あとdev toolsがjotaiで提供されているのは一番大きい魅力、Recoilはdev toolsがないのが最大の不満だった。
便利そう。
これでReact Queryのkeyを管理するのが現状の最有力候補。
結論: 試す

検討3: 今更だけどReduxを使ってみる
やっとタイトルの話。
そもそもこのタイミングで一回ちゃんとReduxを使ってみるのもありだと感じた。
あんなに使われているのでどうせ便利なんだろうと思ってる(投げやり)
ただ慎重に検討しないといけない点がある。
- 非同期処理はいらない
- useStateと併行で使う
Redux過渡期の混乱はなんとなく見ていたので、結局非同期処理でRedux-thunkかRedux-sagaかみたいな論争があったがこれは絶対にややこしくなるので使いたくない。
非同期処理は現状、React Queryが全部やっているので必要にない。
入門記事とか見ていると自然にRedux-thunkを入れられるので気をつけないといけない。
上記がとても参考になった。
ここでRedux Toolkitを批判しているがここを見なければRedux Toolkitを使ってしまうところだった。
README.mdを読んでいる感じ、immerとかRedux-thunkとか余計なものが入っているので使うならreact-reduxのみだなっとなった。
懸念点としては
- Ducksパターンとかre-ducksパターンとかがめんどうそう
- 結局学習コストは高そう
- useStateとの棲み分けが大変そう
これ書いているときに気づいたがDucksパターンとかいってディレクトリ構成を考えないといけないんですね。
私のチームはフロント専任エンジニアとデザイン寄りエンジニアでフロント構築をしていますが、Reduxを導入したときの学習コストはちょっとバカにできないぐらい高そうだと感じた。
専任エンジニアが覚えたとしても、デザイン寄りエンジニアがReduxの部分を触れないと効率は落ちそうですね。
Modalの開閉管理等でuseStateを使いたいとなるとどこまではuseStateでどこからがreduxの管理下を決めるのはしんどそうだなと。
とりあえずGWにreact-reduxを触ってみようかなーってモチベ。
結論: 試す