こんにちは、アソビューでフロントエンドのテックリードをやってる井上です。
シン・エヴァは先日3回目見てきました。毎回心が浄化されますね。 👼
今日はアソビューの各種アプリケーションのフロントエンド(React&hooks api)で使うAPIクライアントについて改めてまとめてみたいと思います。
📝TL;DR
REST APIに対応するにはこれがシンプルで軽くて良さそう 👍
- fetchは軽いカスタムhooks(useFetch、サンプルコード有り)
- 場面に応じてuseSWRとuseSWRInfiniteも使い分ける
- post、put、deleteなどは軽いカスタムhooks(usePost、サンプルコード有り)
🤔APIクライアントでできるといいこと
※弊社ではREST apiでJSONでやり取りすることが多いのでその前提です。(graphQLのAPIサーバーなどは現状立ててないです)
APIコール時の状態、レスポンスデータ管理
ローディング中やレスポンスデータをlocal stateとして扱うなどの諸々。
Reactコンポーネント側でいちいち受け取る箱(useState)を用意してuseEffect内で受け取ったらsetするなどしたくない。
非同期処理やエラーハンドリング周り処理の共通化
async awaitやtry catchなどは毎回コンポーネント側で書きたくないので内包してほしい。
できればAPIレスポンスのライトなキャッシュ
ローカルで結果のキャッシュができるとUXが向上するシーンありますね。
例えば同じAPIを叩くUIコンポーネントを複数置いた時やSPAで画面行き来する時のちょっとしたローカルキャッシュ、更新後のrefetchを待たずに画面は入力値で更新したい、などの機能。
それらを考えて独自でキャッシュ機能作るとstate複数保持したりしてライフサイクル考えるなどそれなりに複雑になりそうです。
簡単に型付けできてほしい
ジェネリクスでお手軽に、シンプルに型付け。
もちろんhooksで
上述の機能群はカスタムhooksで全部隠蔽してComponent側はシンプルで宣言的な記載で書きたい。hookの顔してないものをimportして使いたくないです、もはや。
😭(ちなみに)今までの構成の課題
2年前頃に設計した構成になりますが、
- apiライブラリ:axios
- 状態管理:redux
- 非同期用ミドルウェア:redux-thunk
を組み合わせて、APIの呼び出し、結果の保持などはreduxのstateで管理してこちらに寄せていました。(デザインパターンはducks)
手数が多すぎる、、
当初(hooksが使えるようになる前)はrecomposeなども使いつつ様々な状態をredux stateで管理していましたが、hooksが使えるようになってから一気にhooksに書き換えていったため、機能ドメインか画面単位で作っていたreduxのモジュールがかなり骨抜きになっていき、最終的にAPIのリクエストと結果の保持しかしてないようなものも現れてきました。
(redux hooksは使ってるもののaction、reducerを介す意味がもはや皆無。一応設計はducksでファイル一つにまとめているものの冗長には変わりない)
やりたいことに対して手数多すぎるし特にシンプルにAPIをfetchしてJSON受け取ってコンポーネントにprops渡すだけの処理でredux使う意味が全くない!(そもそもreduxの用途そういうんじゃない)
😆解決策
良いあり物のライブラリとか無い?
単純なRESTAPIをfetchするだけのhooksのライブラリは特にこれというもの無くて、みんな独自でカスタムhooksを自作するかreact-query、swrのようなキャッシュなどの利点があるライブラリを使うか、だと思われます。
弊社では後述しますがswrは使おうと思いますが、やはりカスタムhooksとの併用を想定しています。
※下に使い方とaxiosをfetcherに使う場合の中身のイメージのコードを貼っています
カスタムhooksでfetch処理(useFetch)
みんながみんな思いつく(?)useFetchという名前で作りました。
必要な機能
- urlを指定してリクエスト投げる
- リクエストの結果が返ってきたらdataに値を詰める(useStateでhooksのコンポーネントstateで管理してる)
- リクエスト中はtrue、レスポンス返す際にfalseなisLoading
- エラーが返った場合にtrueに なるhasError、APIのエラーレスポンスを詰めるerrorMessage
これも欲しいよねの機能
- デフォルトはuseEffectでコンポーネントがマウントされたタイミングでのAPIコールで良いが、そうではなく条件に応じて呼びたい
- optionでskip: boolean設定 & 任意のタイミングでfetchするためのrefetch関数を返す
- この辺の作りはApollo clientのuseQueryを参考にしました
postやput、deleteなどの対応
一方登録や更新などuseFetchとは少し使い方が違ってくるので別でusePostというhooksを作りました。
必要な機能
- 任意のタイミングで実行するdoPost関数
- リクエストmethodの指定
- 成功時のコールバック関数onSuccess、失敗時のコールバック関数onError
- 二重submit防止やloadingUI用にisLoading
SWR
hooksでfetchと言えば機能に優れたswrやreact-queryなどのいくつかの候補があります。
この2つができることはかなり似てますし、甲乙つけがたいのですが、SWRはNext.jsのvercel社のチームが散々叩いてきたとのことなので品質が(多分)良いということ、APIのシンプルさと若干のファイルサイズの軽量さで弊社での用途的に swrの方が少し優れていると判断しました。
このブログ書いてる時に気づいたのですが、swrのサイト日本語翻訳されてる!
SWR: React Hooks for Data Fetching
"SWR" という名前は、 HTTP RFC 5861 で提唱された HTTP キャッシュ無効化戦略である stale-while-revalidate に由来しています。 SWR…
SWRのいいところ、使い所
- キャッシュを楽に柔軟に利用できる
- mutateを活用すると工夫次第でUXを更に向上できる(即座にローカル更新してその後に再検証するなど)
- revalidateOnFocusなどを使ってサーバー状態と緩やかな同期を取ってクライアント側でrevalidateのタイミングを意識しない設計にできる
- 無限ロードはuseSWRInfinite使うと楽です。
カスタムhooks(useFetch、usePost)と使い分けるところ
- 上述のキャッシュ機能などが活かせる場所はSWR
- useSWRは任意のタイミングでfetchし直したり、パラメーター変えてfetchするなどの融通が聞きづらいのでそういう用途が必要な場合はカスタムhooks
- 「データ取得のためのReact Hooksライブラリ」とあるように、登録や更新などには適さないので前述のusePostで対応します。
🔚まとめ
一度React hooksに慣れるとhooksじゃない形式のものをcomponent内に書きたくない!と思うのが今のフロントエンドエンジニアの常だと思いますが、APIクライアントもこういう形で作ればできますね!というものの紹介でした!
もっと色々機能をもたせようとすると今回のサンプルコードでは足りない部分もあると思いますが、シンプルな用途には耐えると思います。
💻We’re hiring!
アソビューではエンジニアを(常に)大募集しています!ご興味ある方はぜひお気軽にエントリーしてください!
🐧Appendix
コードサンプルです。
※色々使う中で改修しており、ちゃんと動かなかったらごめんなさい。(実際プロダクションで使ってるやつちょいちょい修正してる)ニュアンスが伝わればと、、
useFetch.ts
usePost.ts