React + Rails API構成でServer Stateの責務を考え、TanStack Queryを採用した話
はじめに
本記事は、前回の記事「React + Rails API構成で作る認証状態の責務を考え、zustandを採用した話の続きとして、ポートフォリオアプリを作成した際に得た知見をもとにアウトプットとして執筆したものです。
注意
本記事は、実務未経験のポートフォリオ開発を経て、得た知見を基に作成しています。
拙ない表現や、認識の甘さが見つかった際はご教授いただけると幸いです。🙇♀️
詳しくは、記事リンク、および、GitHubリンク、アプリリンク先にて確認していただくと幸いです。
GitHubページ :https://github.com/yuji-2293/ColorMirror_Re
アプリへのリンク: https://color-mirror-re.vercel.app/
Stateの責務分類
本アプリの状態管理の責務の違い
| 状態 | 状態管理の方法 |
|---|---|
| ユーザーの認証状態 | zustand |
| リダイレクト理由(UI) | zustand |
| form input欄 | useState |
| params作成 | useState |
| API 登録・取得データ | TanStack Query |
上記の表より、本アプリのState戦略として、以下2つに分類します
Client State
ブラウザ側で扱われる状態
- formの入力値
- API利用時のparams
- 認証状態
- リダイレクト理由
これらはブラウザ側で保持・更新される状態で、サーバーを由来としていない状態です
本アプリでは、Client Stateを以下の2つに分けて分類して責務をさらに分割しています
- componentで扱われる状態を→useState
- アプリ全体で扱う状態を→zustand
Server State
- APIを経由して取得した一覧データ
- API経由で登録・更新したデータ
など、APIより取得したサーバーに保持されるデータをServer Stateと扱っています
フロント側はサーバーにAPIリクエストを行い、データを- 取得
- キャッシュ
- 再取得
- 更新後の同期
を行いUIを構築します。
本アプリではこれらのServer Stateを管理するため、TanStack Queryを使用しています`
なぜuseState + useEffectで管理しなかったのか?
useState + useEffectの併用で同じServer State管理は可能です。
しかし、本アプリでは、Server Stateを複数の機能で必要としていました。
- colorモデル(色に関する機能)
- responseモデル(OpenAI APIの生成レスポンス)
その時、
- useEffectによるAPI通信のための記述が繰り返し記述されてしまう
- 取得から、
loading、errorまで全ての状態をuseStateに入れて管理する必要がある - データの更新後、再取得処理を手動で実装しないと、データの表示が古いままとなる
- Server Stateを複数のコンポーネントで扱うとき、キャッシュ戦略を考えるコストが発生する
といった課題が発生する
本アプリでは、これら課題をシンプルに解決し、且つ、Server Stateを管理できる、TanStack Queryを採用しました。
TanStack Queryが担う責務
以下は、本アプリで採用したTanStack Query に任せた責務です
- サーバーからデータの取得
- キャッシュの一元保持
-
invalidateQueriesを使用して更新後の同期 - loading / error / success 状態の管理
- mutation実行時の状態管理
これらの管理をTanStack Queryで担うことで、
フロント側が取得したデータを元にUIを構築することに専念できる構成を取りました
実際の構成
構成図
責務の分離
本アプリではServer Stateを扱うために、責務を以下のように分離している
- axios → 通信責務
- API関数 → エンドポイント責務
- カスタムフック → Server State責務
- Component → 表示責務
これらの責務をそれぞれディレクトリ構成に落とし込んで責務を担わせている。
実際のコード
API Client(通信責務)
API Client = axios共通処理
src/app/lib/apiClient.ts に置くことで、
ApiClient.get()
ApiClient.post()
のようにaxiosインスタンスを呼び出せる。
API関数でaxiosを呼び出すのでなく、共通インスタンスを経由することで責務を分離して一元管理している
import axios from 'axios';
import applyCaseMiddleware from 'axios-case-converter'; // axiosのレスポンスデータのキーをキャメルケースに変換するミドルウェア
import Cookies from 'js-cookie';
const baseURL = import.meta.env.VITE_API_BASE_URL || '';
const headers = {
'Content-Type': 'application/json',
};
const options = {
ignoreHeaders: true, // レスポンスヘッダーは変換しない
};
export const ApiClient = applyCaseMiddleware(axios.create({ baseURL, headers }),
ApiClient.interceptors.request.use((config) ...(以降省略)
API関数(エンドポイント責務)
ファイル初頭でApiClientをimportすることで、axiosインスタンスを使用している
API関数のファイルでは、通信するだけの責務を担う
ここでServer Stateを扱わない、呼ばれたらサーバーに向けて通信するのみ
import { ApiClient } from '@/app/lib/apiClient';
import { type ColorResponse } from '@/app/features/colors/types/Color';
export default async function colorsGetData(): Promise<ColorResponse> {
try {
const response = await ApiClient.get<ColorResponse>('/colors');
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
カスタムフック(Server State責務)
取得→扱いやすい形に整形→返却までの責務を担う
const colors = query.data?.data || [];
この整形によって、Component側にAPIのレスポンス構造を知らせずに渡すことができる。
→ これによりUIがAPIのレスポンス構造の責務から分離した状態で構築され表示のみの責務を持つことができている。
// 扱いやす形に整形している部分、queryの中の配列を取り出して[colors]に格納
const colors = query.data?.data || [];
//Component側で以下のようにして値を扱える
colors.colorName
colors.mood
import { useQuery } from '@tanstack/react-query';
import colorsGetData from '@/app/features/colors/api/colorsGetData';
import { type ColorResponse } from '@/app/features/colors/types/Color';
export function useColors() {
const query = useQuery<ColorResponse>({
queryKey: ['colors'],
queryFn: colorsGetData,
});
const colors = query.data?.data || [];
return {
...query,
data: colors,
};
}
Component(表示責務)
下記のisLoadingやisErrorがTanStack Queryで管理するServer State
その状態の分類によって切り替えるUIを定義している。
また、カスタムフックにより返却されたdataを読み込み利用することでUIを構築する
import { useColors } from '@/app/features/colors/hooks/useColors';
export const ColorsIndex = () => {
// データの読み込み中やエラーが発生した場合の表示
const { isLoading, isError, data } = useColors();
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>エラー、ファイル、データの確認をしてください</div>;
}
const colorsIndex = data || [];
console.log(colorsIndex);
return (
<ul>
{colorsIndex.map((color) => (
<li key={color.id}>
<div style={{ backgroundColor: color.colorName }} />
<p>{color.mood}</p>
<p>{color.response?.aiResponse}</p>
</li>
))}
</ul>
);
};
実装してみてわかったこと
本アプリの開発に伴い、状態管理ライブラリは単に状態管理を便利にするためのものではなく、管理する状態の性質によって使い分けることで、コードの可読性や責務の分離に大きく関わるものであることを学びました。
調査している当初、状態管理はzustandのようにstoreで管理するものと考えていました。
しかし、実装を進める中で、
- Client State
- local(useState)
- global(Zustand)
- Server State
- APIから取得したデータ
- TanStack Queryによる取得・キャッシュ・再取得・同期
のように性質を分類できることに気づきました。
そこで、本アプリではTanStack Queryを上記のような複雑な管理が必要なServer Stateの管理を担わせることで、責務をきちんと分離し、Component側に取得したデータを元にUIを構築することに専念させることができました。
今回の開発を通して、「どの状態がどの責務を持つか」を性質によって整理することの重要性を学ぶことができました。
まとめ
本記事では、React + Rails API構成におけるServer Stateの責務について整理しました。
- Client StateとServer Stateの性質の違い
- Server Stateを扱う際は、責務を
- axios(通信)
- API関数(エンドポイント)
- TanStack Query(状態管理)
- Component(表示)
を用いて分離することで、フロントにUIの構築に専念できる構成を実現できる
前回の記事では認証の状態をzustandで管理することを整理してまとめました、今回の記事を通して、状態の性質で管理手法を決める重要性を学ぶことができました。
終わりに
ここまで読んでいただき、ありがとうございました。
本アプリの開発を通して非常に多くの気づき、知見を得ましたので随時執筆を行い、公開していきたいと思います。
GitHubリンク: https://github.com/yuji-2293/ColorMirror_Re
アプリURL: https://color-mirror-re.vercel.app/
次回 React + Rails APIでのリプレイス開発を通して、責務設計の変化について