React開発におけるサーバーステートマネジメント
こんにちは、株式会社Gizumoでエンジニアをしている田中です。
現在React(Next.js)で開発している自社サービスで「TanstackQuery」ライブラリの採用が決まりました。
社内で使用経験のないライブラリであることから社内の共通認識を持つ目的を兼ねて、TanstackQuery
とは何か、どのように扱うのかを見ていきます。
目次
前提・対象とする読者
この記事は以下を前提として読み進めてください。
- サンプルコードは
React
とTypeScript
で記述 - Reactが提供する機能について等の解説は最小限で止める
また、対象とする読者は以下のような方を想定しています。
React
(JavaScript
で可)や他のフロントエンドフレームワーク
(以下FW
)の使用経験がある方FW
で用いられる「状態(state
)」についてなんとなく理解がある方React
の状態管理設計について検討中の方
TanstackQueryとは
TanstackQuery(旧ReactQuery)とはフロントエンド開発で用いられる「データフェッチングに伴う処理をサポート」するためのライブラリです。あくまでサポートの立ち位置であり、fetch
関数やAxiosなどと併用することを前提とした設計になっています。
サポートの主な目的は「状態管理」です。
状態管理の中でも「サーバーから取得したデータの状態管理」をメインに扱います。
TanstackQueryの特徴
TanstackQueryの特徴は大きく分けて2つあります。
1) 宣言的記述
TanstackQueryを用いれば宣言的な記述によりコードの見通しがとても良くなります。
具体例として初期表示でデータを取得する処理をTanstackQuery
を「使用する例」と「使用しない例」で記述してみます。
前提としてデータ、ローディング、エラーの状態が伴うものとします。
【TanstackQuery
を使用しない例】
import { fetchTodos } from '@/my-api';
import { Todo, MyErrorType } from '@/types';
const MyComponent: FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<MyErrorType>(undefined);
const _initAsync = async () => {
setIsLoading(true);
try {
const fetchData = await fetchTodos();
setTodos(fetchData);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
_initAsync();
}, []);
if (isLoading) return <div>Loading...</div>
if (error) return <div>{error.message}</div>
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
【TanstackQuery
を使用する例】
import { useQuery } from '@tanstack/react-query';
import { fetchTodos } from '@/my-api';
import { Todo } from '@/types';
const MyComponent: FC = () => {
const { data, isLoading, error } = useQuery<Todo[]>(['todo'], {
queryFn: fetchTodos,
});
if (isLoading) return <div>Loading...</div>
if (error) return <div>{error.message}</div>
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
両者を比較してみる
TanstackQuery
を使用しない例
では、useEffect
を使ってuseState
で定義したstateを更新する手続きを行なっています。
TanstackQuery
を使用する例
では、それぞれの状態をuseQuery
メソッドから取得しているだけです。また、stateをいっさい定義していません。
余計な手続きを捨てよう
公式は下記のように謳っています。
Toss out that granular state management, manual refetching and endless bowls of async-spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that directly improve both your developer and user experiences.
こちらを翻訳すると、
きめ細かな状態管理、手動での再フェッチ、延々と続く非同期スパゲッティ・コードを捨てましょう。TanStack Queryは、宣言的で常に最新の自動管理されたクエリとミューテーションを提供し、開発者とユーザーの両方のエクスペリエンスを直接向上させます。
とのことで、TanstackQuery
でデータフェッチの宣言的記述を行うことでプログラム上の手続きの数が減り、React
が管理するstate
の数を減らすことができます。
宣言的記述の例でもその違いを実感いただけたのではないでしょうか。
2) 状態管理
状態管理はTanstackQuery
最大の特徴です。
状態管理をTanstackQuery
に任せる上で知っておくべき4項目を解説します。
- サーバーの状態を管理する
- ブラウザのキャッシュ機構を利用する
- 状態へのグローバルアクセスが可能
- 専用のdevToolsを利用する
サーバーの状態を管理する
状態管理といえばReact Hooks
(useState
, useReducer
)やRedux
等の状態管理ライブラリが思い浮かびます。React Hooks
やRedux
では「UIの状態」や「サーバーから取得したデータの状態(※以下より「サーバーの状態」)」を管理できますが、TanstackQuery
は「サーバーの状態」にスコープを絞って状態管理を行います。
よって状態管理の分野においてReact Hooks
やRedux
との分業が可能になり、React Hooks
やRedux
は「UIの状態」を管理し、TanstackQuery
は「サーバーの状態」を管理してそれをReact
のコンポーネントから利用するという構図を作ることができます。
ブラウザのキャッシュを利用する
TanstackQueryはWebブラウザのキャッシュ機構を利用した状態管理を行います。
キャッシュを利用するため、画面を更新すると保存したデータは削除されます。
その面でLocalStorage
やSessionStorage
とは用途が異なります。
Stale while Revalidate
TanstackQueryのキャッシュを利用した状態管理はStale while Revalidate
という考えのもと行われます。
Stale while Revalidate
は「キャッシュデータの鮮度を確認し、キャッシュデータが古い場合、一時的にキャッシュデータを返しておいて、裏で通信を行い最新のデータを返す、キャッシュデータが古くなければそのままキャッシュデータを返す」という考え方です。
とてつもなくややこしいですが、この挙動によって「初期表示以外はデータが表示され続けているためUIが大きく変更されない」というUXを実現できます。そのため初期表示以外はローディングUIを必要としません。
TanstackQueryオプションの設定例
「キャッシュデータの鮮度」はTanstackQueryオプションのstaleTime
で設定できます。
デフォルトは0で設定されており、Infinity
まで指定可能です。
staleTime:0
キャッシュデータを常にstale
(古い)とみなし、画面遷移のたびにリフェッチを行いサーバーから最新のデータを取得します。
staleTime:Infinity
キャッシュデータを常にfresh
(新鮮)とみなし、画面遷移を何度行おうとリフェッチを行わずキャッシュデータを返します。画面更新等でキャッシュデータが破棄されるまで、初期表示で取得したデータが常に表示され続けます。
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity
},
},
});
状態へのグローバルアクセスが可能
TanstackQuery
で管理する状態はグローバルにアクセスが可能です。
「グローバルに」というのは、階層関係なくどのコンポーネントからでも平等にアクセスが可能ということです。そのためコンポーネント間のProps
を必要としません。
この仕組みを利用することでサーバーの状態管理のためにRedux
等の状態管理ライブラリ(store
)を用意する必要がありません。
専用のdevToolsを利用する
TanstackQuery
には専用のdevTools
が用意されています。React
であれば下記コマンドでインストールして利用できます。
npm i @tanstack/react-query-devtools
TanstackQuery
の状態は一意なキーに紐づいて管理され、それをdevTools
上から確認できます。
また、状態のデータ詳細だけでなく、その状態がfresh
(新鮮)かstale
(古い)かやリフェッチが行われているタイミングなど、TanstackQuery
を用いて状態管理する上で必要な情報をまとめて確認できます。
※画像右中央Data Explorer
中Data
は左上メニューからstale
(古い)状態であることがわかります。
TanstackQueryの使い方
TanstackQueryの使い方は大きく2つに分けることができます。
それぞれどのように行うのか簡単に見ていきましょう。
1) 状態の参照と登録 useQuery
状態を参照・登録するには、useQuery
を使用します。
const { data, isLoading, error } = useQuery<Todo[]>(['todo'], {
queryFn: fetchTodos,
});
useQuery
の第1引数にはキャッシュに保存してある状態と紐づく「一意なキー」が必要です。
画面の初期表示時、useQuery
が一番最初に実行されたタイミングでキャッシュにデータが登録され、指定したキーが登録されたデータと紐づけられます。
useQuery
を用いてデータを参照する際はStale while Revalidate
が働くため、初期表示以外は通信を待つことなくデータが表示されたUIを維持できます。
2) 状態の更新 useMutation
下記コードは選択したTodoを更新する例です。
キャッシュの状態を更新するにはTanstackQuery
からuseMutation
というHooksを利用します。useMutation
の第1引数には更新用のAPIが入ります。useMutation
を実行することで第1引数のAPIが実行されバックエンドの更新が行われます。
第2引数のonSuccess
メソッドはAPIの処理が成功後実行されます。引数にはAPIの実行結果が渡されます(data
)。
const updateTodoMutation = useMutation({
mutationFn: updateTodo,
onSuccess(data, variables) {
...
},
});
キャッシュデータの上書き
useQueryClient
を使用することで現在のキャッシュにアクセスするためのオブジェクトを取得できます。
※サンプルコードでは取得したオブジェクトをclient
という変数に格納しています。
client.setQueryData
でキャッシュデータを上書きできます。第1引数には対象の状態と紐づくキーを指定し、第2引数のコールバック関数から返された値がキャッシュにセットされます。
今回は状態の更新を例にとって実装していますが、キャッシュのデータを更新するという意味では作成も削除も変わりません。更新・作成・削除のイベントに合わせて、キャッシュのデータを更新することでキャッシュとサーバーの状態を合わせます。
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateTodo } from '@/my-api';
import { UpdateTodoDTO } from '@/types'
// 現在のキャッシュデータにアクセスするためのオブジェクト
const client = useQueryClient();
// 定義
const updateTodoMutation = useMutation({
mutationFn: updateTodo,
onSuccess(data, variables) {
client.setQueryData<Todo[]>(['todo'], (prev) => {
if(!prev) return []
return prev.map((todo) => {
if (todo.id === data.id) return data;
return todo;
});
});
},
});
// 使用
const handleUpdate = ({ id, title, completed }: UpdateTodoDTO) => {
updateTodoMutation.mutate({ id, title, completed });
};
TanstackQueryの使い方まとめ
状態の参照と登録
useQuery
を使用します。初期表示に一度呼ぶことでキャッシュへのデータの登録が行われ、Stale while Revalidate
によってデータの鮮度に合わせて状態が返されます。
状態の更新
useMutation
を使用します。setQueryData
を使用することによってキャッシュのデータを更新できます。- データの更新をしたい時、新しくデータを作成した時、データを削除した時にサーバーの処理に応じてキャッシュデータを更新する必要があります。
最後に
今回、弊社の自社開発アプリケーションでは以下の理由でTanstackQuery
の採用が決まりました。
- 複雑な状態管理ライブラリの代替となる
React
のSuspense
やErrorBoundary
と相性が良い- モダンフロントエンド開発でスタンダードに採用されている傾向にある
はじめて使用するライブラリなので、知見を溜めつつ、実装する上で見えてくるもの、設計に関することについて今後当ブログで共有できたらなと思います。
ここまで読んでいただきありがとうございました。