Reduxを完全に廃止し、Apollo Clientに乗り換えた話
こんにちは、フロントエンドエンジニアの池田です。 前回のWistantのフロントエンド技術スタックを刷新した話に引き続き、 今回はWistant上でReduxを廃止した背景と、刷新の経緯について話してければと思います。
Redux廃止の背景
Reduxはもう必須ではない
前提として、前の記事で書いた内容を再び述べようと思います。
Global Stateの管理はモダンなWeb開発において、重要なトピックとなってます。 Redux は長らく React を使う上でのGlobal Stateのデファクトスタンダードになってましたが、 それに対する様々なアプローチが可能になった(Apollo Client や React Hooks の登場)今は、最近はその傾向が薄れて来ました。
全ページのUI改修
同じく、前の記事で述べたのですが、全ページのUIを改修するタスクに伴い、一からフロントエンドの基盤を作り直すことで、今まで積み残されていた技術的負債や開発者経験(DX)の改善も同時に行っていくことが決まりました。その技術選定を改める際、Reduxを廃止することを決定しました。
Reduxを使いながら感じた問題点
開発者経験(DX)の低さ
単純に実装量が多いことが開発者経験を妨げていました。 例えば、以前の技術スタックでは新しいエンティティーのAPIを追加する場合、以下のプロセスが必要でした。
1. reducerを作る(action types、action creator含む) 2. entityのnormalizeのためのコードを書く 3. GraphQLのquery/mutationを書く 4. APIを呼び出す関数を書く 5. 4を呼び出すredux-sagaのコードを書く 6. selectorを書く
新しいentityではない場合でも、3〜6の過程が必要になり、UI/UX以外での実装コストが高く感じました。 また、APIの改修が必要になった時も、ファイルが色んな場所に分散されていたため、修正箇所がわかりづらい場合が多々ありました。
バンドルサイズの増加
アプリケーションの規模が大きくなるに連れ(エンティティーの増加)、reducer
の数も増加しました。問題はバンドルした時の root.js
のサイズも共に上がり(他の問題も重なったりして、parsedで3 MB以上に及ぶ大きさを誇ってました)、アプリケーションのパフォーマンス(主にファーストペイント)に影響している点でした。
ユーザーが最初にアクセスするページはログイン画面またはホーム画面で、その時必要なreducer
のみダウンロードすれば良いのですが、すべてのreducer
を combineReducers
でまとめてるため、それができてない状態でした。
コードスプリッティング すれば良い話では?と言われればその通りですが、最初からそれができるアーキテクチャーにしておらず、改修に多くのコストが掛かる恐れがあったため、着手できてませんでした。
他の技術との噛合
v5.0を開発する際に、 Apollo Client
や Relay
など、GraphQL
のリクエストと状態管理を楽にしてくれるライブラリーを使用することを決めていたので、Reduxを引き続き使用する必要性を感じませんでした。
ReduxをどうReplaceしたか
実装量が多いかつコードが分散されていることが歪みとしてあったので、それらをコンパクトにまとめることにしました。
バンドルサイズは、reducer
を廃止し、コードスプリッティングを徹底することで解決できました。(parsedで1 MB以下になりました)
APIリクエストとデータをApollo Clientに任せる
Redux
廃止前は、fetch
してきたデータをストアに保存し、normalize
まで独自で管理してましたが、
Apollo Client
ではデータのキャッシュを自動で管理してくれるので、そこらへんを考えなくても良くなりました。
また、APIリクエストは実際それを使用するページやコンポーネントにまとめることで、どこを修正すれば良いのかをわかりやすくしました。
Redux
を使ってたときは、APIリクエストで返ってきたデータを Store
に入れて、Selector
を使ってUIに適用する形でしたが、Apollo Client
はデータドリブンなUIに特化してるので、APIリクエストで返ってきたデータをそのままUIに適用する実装にしてます。
しかし、どうしてもキャッシュされたデータが必要な場合があります。(例:ユーザー検索など)
そういう時は readFragment
などのメソッドを使用すればOKです。
Redux
// selector const userSelectorFactory = id: string => createSelector( allUsersSelector, allUsers => allUsers.find(user => user.id === id) ); // usage return connect(createStructuredSelector({ user: (_, props) => userSelectorFactory(props.userId) }))(UserComponent);
Apollo Client
const __typename = 'User'; const UserFragment = gql` fragment __User on User { id displayName avatarUri status roles } `; // util const getUserFromCache = ( userId: string, fragment: DocumentNode = UserFragment, ): User | undefined => { return client.readFragment({ id: `${__typename}:${userId}`, fragment, }); }; // usage const user = getUserFromCache(userId);
注意する点としては、readFragment
とか readQuery
は、必ずそのデータや Query
が呼ばれていることを前提としているため、使い場所を選ぶということですね。
Conext APIとReact HooksでGlobal Stateを管理する
Global State
は、ユーザーの情報をアプリ上で管理するために必要でした。
その代案としては MobX
や Apollo Client
のローカルステートなどが挙げられますが、今回は Context API
と React Hooks
を使うことにしました。
選定理由は別途のライブラリーが要らず、学習コストがかからないためです。
Redux
// reduxのconnectを使う(v7.1.0以前) return connect(createStructuredSelector({ me: meSelector }))(Component);
Apollo Client
// useContextを使う const { me } = useContext(MeContext); // or custom hooksを使う const me = useMe();
useContext
を使えば、どのコンポーネントでも簡単に Global State
にアクセスできるので、便利ですね。
Redux
も v7.1.0でhooksに対応するようになり、useSelector
などで同様の書き方ができますが、Wistantではv5.0.5を3年前から使っていたので、そのような実装はできてませんでした。
気になったのは、アプリケーションのGlobal State
の Context
を分解しているため、Provider
が無限に重なっていくこととですね……。
const App = () => { return ( <ThemeProvider> <NowProvider> <HideProvider> <ModalProvider> <SideSheetProvider> <PulldownProvider> <EditProvider> <AppRouter /> </EditProvider> </PulldownProvider> </SideSheetProvider> </ModalProvider> </HideProvider> </NowProvider> </ThemeProvider> ); };
ネストされた Provider
をまとめる方法もいくつかあったのですが、そこまでクリティカルな問題でもないので、一旦このままにしております。
Redux-Formについて
Redux
を使うならRedux-Form
も一緒に採用される傾向がありました。
Wistantでも Redux-Form
を使ってた時代がありますが、Redux
廃止の6ヶ月以上前に Formik
に移行しました。
Redux
および Redux-Form
の作者であるDan Abramov氏やコミュニティーの結論は、フォームの状態管理を Global State
で管理すべきではないということです。レポジトリの README.md
にもその旨のメッセージが書いており、React Final Form
などを使うことを推奨しています。
最後に
Redux
は現在もグローバルな状態管理をするために使えるツールですが、Context API
の登場後、React
の基本APIで同じような実装が可能になってるので、今後自分が採用することはないと思います。最近話題のFacebook製の状態管理ライブラリー Recoil
などは今度試してみたいですね。廃止後に登場したため候補には入らなかったので。