この度TanstackQueryを学ぶ必要がでてきたので備忘録的にTanstackQueryについてその中でも今回はuseQueryというフックに焦点を当てました。
これまではreactでデータのフェッチはuseEffect+useStateの組み合わせを使ってきたのですが、画面を移動するたびに読み込みが発生して遅延が発生している状況なので、ローディングとキャッシュを用いてパーフォマンスを良くしたいというきっかけで調べ始めました。
Reactでのデータフェッチでは、React が推奨してるように多くの場合キャッシュ機能を搭載した3rd partyライブラリを利用した方がいいようです。
公式ドキュメント
TanstackQueryとは?
TanStack Query(旧称 React Query)は、React等のJavaScriptフレームワーク向けの非同期の状態管理ライブラリです。
TanStack Query の主な特徴
- データフェッチングの管理
サーバーからのデータの取得を簡略化する。
自動的にデータをキャッシュし、再フェッチするタイミングを管理する。 - キャッシュの効率化
フェッチしたデータをクライアント側にキャッシュし、再利用可能にする。
必要に応じてデータを自動で更新する仕組みを持つ。 - 状態管理の軽減
サーバーのデータをクライアントで扱う状態管理を最適化する。
React Context や Redux のような状態管理ライブラリと比べ、サーバーからのデータに特化している点が特徴。 - 再フェッチと同期
ページがフォーカスされた際やデバイスがオンラインに戻った際に自動でデータを再フェッチする。 - エラー処理
サーバーからのエラーやフェッチ失敗を効率的にハンドリングできる。
TanStack Queryはデータフェッチライブラリではない
TanStack Queryはデータフェッチングライブラリと思われることがあるようですが、TanStack Query自体はデータの取得は行いません。
TanStack Queryはデータフェッチングライブラリではないというのはコードを見れば分かりやすくなります。
以下のqueryFnの部分の関数はデータを取得する必要があるたび実行されるのですがそこでaxiosが使われています。axiosはデータフェッチングライブラリです。つまりこれはTanStack Query自体がデータフェッチを行っていない証拠です。
const { data, isPending } = useQuery({
queryKey: ["issues"],
queryFn: () => axios.get("/issues").then(res => res.data)
})
TanStack Queryはデータの取得がどのようにされるかを気にしません。 唯一関心があるのは、queryFnにPromiseが返されることだけです。
Promiseが返されることだけが関心なため、データフェッチにはaxios、fetch、graphql-request などのデータ取得ライブラリを使ってPromiseを渡せば問題ないです。
TanStack Queryが行う非同期状態の管理とは?
データフェッチングライブラリで無ければ何なのか?という疑問がでてきますが、冒頭で紹介した通りTanStack Queryは非同期の状態(Server State)管理のライブラリです。
TanStack Queryがなぜ作られたかを説明するには、まず状態管理の概念にClient StateとGlobal StateそしてServer Stateという3つの状態が存在することを理解する必要があります。
この3種類は以下のような分類がされます。
- Client State
各Component内で管理される状態
ToggleボタンのON/OFFなど - Global State
ページをまたいで保持し続ける必要のある状態
エラー通知のためのToastなど - Server State
サーバーデータのキャッシュ
ユーザ情報・記事一覧などあらゆるサーバで管理されているデータ
クライアントが完全に所有 | リモートで操作されうる |
---|---|
常に最新の状態 | 状態が古くなる可能性がある |
同期的に利用できる | 非同期に使用する必要がある |
React QueryやSWRといったライブラリの登場以前は、全ての状態をClient StateかGlobal Stateによって管理する必要があり、サーバーデータの管理はGlobal Stateによって行われていました。
状態管理の代表格であるReduxではサーバーから取得するデータもアプリケーション内で使用されるUIの状態も同様に扱われていました。
従来の状態管理ライブラリのほとんどはクライアント状態の処理には優れていますが、非同期状態やサーバー状態の処理にはそれほど優れていません。これは、Server StateがClient State(Global State)と違い以下のような特徴があるからです。
- 自分が管理または所有していない場所にリモートで保存される
⇒状態の所有元が違う - 取得と更新には非同期APIが必要
⇒Server Stateは同期的に管理することが難しい - サーバーの状態は知らないうちに他の人によって変更される可能性がある
⇒アプリケーションが「古くなる」可能性がある
以上のことをふまえると、Client StateとServer Stateを責務が異なる二つの状態をまとめてGlobal Stateで管理していたことが問題だったのです。そこで登場したTanStack Queryは、Client State・Global StateとServer Stateの責務を正しく分割し、Server Stateの非同期で且つ状態が古くなり得る特徴を効率的に扱えるようにしたライブラリと言うことができます。
非同期状態:ローディングやフェッチ中など、サーバー上で管理されるデータの状態
基本的な使い方
ここでは基本的なTanStack Queryの使用について紹介します。
準備
-
インストール
npm i @tanstack/react-query
-
クエリクライアントの設定
TanStack Queryを使用する前に、QueryClientを設定し、TanStack Queryを使用するコンポーネントをQueryClientProviderでラップする必要があります。App.tsximport { QueryClient, QueryClientProvider, } from '@tanstack/react-query' const queryClient = new QueryClient() export default function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ) }
useQuery(データの取得)
データ取得には、useQueryというフックを使います。使い方はuseQueryフックの引数にqueryKeyとqueryFnをプロパティとして持つオブジェクトを渡します。
import { useQuery } from '@tanstack/react-query'
function App() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList
})
}
queryKey
(必須):データを再取得する時やキャッシュ管理などで利用される一意のキーです。データが保存される場所
queryFn
(必須):Promise を返す関数(データを取得するための関数)です。
これ以外にもオプションが存在します。
enabled
:クエリ同士に依存関係を持たせることができます。enabledオプションがfalseの場合はクエリが実行されません。
staleTime
:キャッシュをstale状態(古い状態)にするまでの時間です。number型もしくはInfinityを設定します。デフォルトは0でデータが取得された直後にキャッシュがstale状態になります。
staleTime: 60 * 1000 //freshな状態が1分維持されます。
Infinityを設定した場合はキャッシュが自動的にstale状態になることはなく、freshな状態が維持されます。
staleTime: Infinity
gcTime
: キャッシュをガベージコレクション(メモリ領域の開放)するまでの時間です。number型もしくはInfinityを設定します。デフォルトは5分になっており、Infinityを設定した場合はGC(ガベージコレクション)を無効化することができます。
返り値
useQueryが結果として返すものをいくつか紹介します。
status
:データに関する状態を定義した値で3種類存在します。クエリは、特定の時点で次のいずれかの状態になります。
- pending・・・キャッシュされたデータがなく、クエリの実行が完了していない状態
- error・・・クエリ実行時にエラーが発生した状態
- success・・・表示するデータが存在する状態
これらそれぞれの状態を扱うためのフラグとしてisPending、isSuccess、isErrorというboolean値が提供されています。-
isPending
:status === 'pending'
-
isError
:status === 'error'
-
isSuccess
:status === 'success'
-
data
:クエリがisSuccess状態の場合、データはdataプロパティを介して利用できます。
isFetching
: どのような状態でも、クエリがフェッチ中の場合 (バックグラウンドの再フェッチを含む)、isFetching はtrueになります。
fetchStatus
: statusに加えて、次のオプションを持つ追加のfetchStatusプロパティも取得されます。statusと同じく3種類存在します。data
がデータに関する情報(データがあるかどうか)を提供する一方で、この値はqueryFnの実行に関する状態を定義した値であり、フェッチの実行に関する状態を扱います。
- fetching
queryFnが実行されているときは常にtrueになります。 - paused
クエリを実行しようとしているが、一時停止している状態。 - idle
クエリが実行されていない状態。
isLoading
:isLoadingはisFetching && isPendingと定義することができます。はじめてQueryがFetchを行った時のみ値はtrueとなります。
基本的にisPendingをLoadingの表示に使う
ほとんどのクエリでは、 isPending状態をチェックし、次にisError状態をチェックし、最後にデータが利用可能であると想定して成功状態をレンダリングするだけで十分です。
function Todos() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
if (isPending) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
// We can assume by this point that `isSuccess === true`
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
キャッシュについて
useQueryを用いてAPIがリクエストされた場合のデータ取得(キャッシュ有無)までの流れ
- useQueryがコンポーネントで呼ばれ、データフェッチを開始
- queryKeyを参考にキャッシュを確認する
- キャッシュヒットした場合、キャッシュされたデータをコンポーネントに返却
- キャッシュヒットしなかった場合、または、キャッシュヒットしたがキャッシュがstale状態の場合は3に進む
- バックグラウンドで新しいデータをfetchしてアップデートする
- 新しいデータをキャッシュに保存して再レンダリングする
server Stateをなるべく最新に保つためにキャッシュの生存戦略を考えることが重要になります。
useQueryを試しに使ってみた
以下はJson Placeholderのページのpostsというエンドポイントにあるデータを取得して表示する例です。isPendingがtureの場合はローディングを表示し、isErrorがtrueの場合はエラー表示するようにしています。
App.tsxの中にReactQueryDevtoolsというコンポーネントがありますが、こちらは開発ツールであり。React Query 内部動作をすべて視覚化するのに役立ちます。
↓これでインストール
npm i @tanstack/react-query-devtools
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./App.css";
import Posts from "./components/Posts";
import { useState } from "react";
import Post from "./components/Post";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,//デフォルトでコンポーネントにフォーカスが当たるとフェッチするのを無効化
},
},
});
function App() {
const [postId, setPostId] = useState(-1);
return (
<QueryClientProvider client={queryClient}>
{postId > -1 ? (
<Post postId={postId} setPostId={setPostId} />
) : (
<Posts setPostId={setPostId} />
)}
<ReactQueryDevtools />
</QueryClientProvider>
);
}
export default App;
import { QueryClient, useQuery } from "@tanstack/react-query";
import axios from "axios";
const queryClient = new QueryClient({});
const Posts = ({
setPostId,
}: {
setPostId: React.Dispatch<React.SetStateAction<number>>;
}) => {
const { data, status, error, isFetching } = useQuery({
queryKey: ["posts"],
queryFn: async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
return data;
},
});
if (status === "pending") {
return <p>ロード中...</p>;
}
if (error) {
return <p>エラーが発生しました: {error.message}</p>;
}
console.log(data);
return (
<div>
<h1>ポスト一覧</h1>
<div>
{data.map((post: any) => (
<p key={post.id}>
<a
href="#"
onClick={() => setPostId(post.id)}
//キャッシュされているかの確認。キャッシュがあればこちらのスタイルが適用される
style={
queryClient.getQueriesData(["post", post.id])
? { fontWeight: "bold", color: "green" }
: {}
}
>
{post.title}
</a>
</p>
))}
</div>
</div>
);
};
export default Posts;
まとめ
このようにTanStack Queryを使うことでローディングやデータフェッチ中などの状態に関する処理がTanStack Query内に隠蔽されることによって、こちらで非同期なサーバーのデータに対する状態管理を実装する必要がなくなりました。また、キャッシュ機構により状態を適切なタイミングで更新できるため、Server Stateをできる限り最新に更新することができます。
状態管理ライブラリには他にもあるのでそれぞれの特徴を調べてベストに近い選択ができるようになりたいと思います。
NEXT.jsとかになってくるとクライアント側でデータフェッチは非推奨でsever componentsを使うことがベストプラクティスになってくると思うのでそのあたりもちゃんと理解していきたいです。
参考