はじめに
皆さん、TanStack Query(旧React Query)をご存知ですか?
TanStack Queryはデータフェッチ、データキャッシュを可能にする素敵なライブラリです。
今回は皆さんに紹介したく記事を書きました。
TanStack Queryの利点
使用する前にざっくりと調べてみると、React標準のコンテクストを使用した場合と比較して、下記利点を説明している方が多かったです!
- 描画回数の最適化
- コードがよりシンプルになる
- サーバーとの通信処理が楽、キャッシュの管理も自動で実施
何やら良さげな雰囲気がしますね・・・!
コンテクストを使った場合と比較して良さを確認していきたいと思います!
使用した環境
- Node.js 19.1.0
- React 18.2.0
- @tanstack/react-query 4.293
- axios 1.3.6
- react-router-dom 6.10.0
- express 4.18.2
- cors 2.8.5
事前準備
サーバー側の準備
データフェッチ用の簡易的なサーバーを作成しましょう。
Expressを使って作成します。
npm install express cors
リクエストを受けたらユーザーを返すプログラムを作成します。
const express = require("express");
const cors = require("cors");
const app = express();
const router = express.Router();
//corsの設定
app.use(cors());
app.listen(3000);
router.get("/", (req, res, next) => {
//テストデータを返却
res.json([
{
id: 1,
name: "tanaka",
description:"高身長"
},
{
id: 2,
name: "sato",
description:"体格が良い"
},
]);
});
app.use(router);
実行して動作確認しておきましょう。
node ./server.js
取得できていますね!次はクライアント側の準備を進めていきます。
クライアント側の準備
viteを使ってサクッとプロジェクトを作成します。
npm create vite@latest
プロジェクトを作成したらプロジェクト階層に移動して必要なライブラリをインストールします。
今回はtanstack query
とHTTPライブラリのaxios
、擬似的な画面遷移を行いたいのでreact-router-dom
を入れておきます。
npm install @tanstack/react-query axios react-router-dom
コンテクストを使った場合
まずはコンテクストを使用した場合を見ていきましょう。
ソース
コンテクスト
サーバーから取得したユーザー情報を管理するState,Context
を作成します。
import { useContext, useState, createContext } from "react";
const UserDataContext = createContext();
const useUserDateContext = () => useContext(UserDataContext);
const UserDataContextProvider = ({ children }) => {
//ユーザー情報を管理するState
const [users, setUsers] = useState([]);
return (
<UserDataContext.Provider value={{ users, setUsers }}>
{children}
</UserDataContext.Provider>
);
};
export default UserDataContextProvider;
export { useUserDateContext };
カスタムフック
次にサーバーからデータをフェッチし、State
を保存するカスタムフックを作成します。
ユーザー情報のデータとset関数は作成したコンテクストから取得します。
コメントにも記載しましたが、State
の変更が3回発生します。
import { useEffect, useState } from "react";
import { useUserDateContext } from "../Contexts/UserDataContext";
import axios from "axios"
export const useContextFetch = () => {
//コンテクストからユーザー情報、ユーザー情報set関数を取得する
const { users, setUsers } = useUserDateContext();
//エラーかどうか判定するState
const [isError, setIsError] = useState(false);
//読み込み中かどうか判定するState
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchUserData = async () => {
setIsError(false);
//State変更1:false→true
setIsLoading(true);
try {
//サーバーと通信
const res = await axios("http://127.0.0.1:3000/");
//State変更2:[]→res.data
setUsers(res.data);
} catch (error) {
setIsError(true);
}
//State変更3:true→false
setIsLoading(false);
};
fetchUserData();
},[setUsers]);
return { users, isLoading, isError }
};
画面コンポーネント
描画されたことを検知できるようにconsole.log
でコメントを残しておきます。
取得したユーザーの一覧と詳細画面を作成します。
一覧
カスタムフックからユーザー情報を取得して表示します。
isLoading,isError
がtrue
の場合はそれぞれメッセージを返します。
import { useContextFetch } from "../hooks/useContextFetchHook";
import { Link } from "react-router-dom";
const ContextFetch = () => {
//カスタムフックからユーザー情報を取得。
const { users, isLoading, isError } = useContextFetch();
console.log("ContextFetchが描画されました。");
//読み込み中
if (isLoading) return <div>{"Loading..."}</div>;
//エラー発生時
if (isError) return <div>{"Error"}</div>;
return (
<>
<h1>ユーザー一覧</h1>
<ul>
{users?.map((user) => {
return (
<li key={user.id}>
<Link to={`/detail?id=${user.id}`}>{user.name}</Link>
</li>
);
})}
</ul>
</>
);
};
export default ContextFetch;
詳細
詳細画面では一覧画面表示時に取得したユーザー情報をコンテクストから取得します。
import { useUserDateContext } from "../Contexts/UserDataContext";
import { useLocation } from "react-router-dom";
const ContextFetchDetail = () => {
console.log("ContextFetchDetailが描画されました。")
//コンテクストからユーザー情報を取得
const { users } = useUserDateContext();
//URLパラメータからユーザーIDを取得し対象ユーザーを抽出する
const { search } = useLocation();
const urlParams = new URLSearchParams(search);
const id = Number(urlParams.get("id"));
const user = users.find((user) => user.id === id);
return (
<>
<h1>ユーザー詳細</h1>
<table>
<thead>
<tr>
<td>ID</td>
<td>名前</td>
<td>説明</td>
</tr>
</thead>
<tbody>
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.description}</td>
</tr>
</tbody>
</table>
</>
);
};
export default ContextFetchDetail;
App.jsx
各コンポーネントで作成したコンテクストを使用できるようにUserDataContextProvider
を設定します。
import ContextFetch from "./components/ContextFetch";
import UserDataContextProvider from "./Contexts/UserDataContext";
import ContextFetchDetail from "./components/ContextFetchDetail";
import { BrowserRouter, Route, Routes } from "react-router-dom";
function App() {
return (
<BrowserRouter>
{/*コンテクストのProvider*/}
<UserDataContextProvider>
<Routes>
<Route path="/" element={<ContextFetch />} />
<Route path="/detail" element={<ContextFetchDetail />} />
</Routes>
</UserDataContextProvider>
</BrowserRouter>
);
}
export default App;
これで実装完了です!動作確認して挙動を見ていきましょう。
動作確認
下記コマンドで実行します。
npm run dev
一覧
ちゃんと表示されていますね!ちなみに一覧画面は何回描画されたのでしょう・・・?
コンソールを確認してみます。
4回描画されていますね!実は初回描画以外は下記処理実行時に行われています。
useEffect(() => {
const fetchUserData = async () => {
setIsError(false);
//State変更1:false→true(*2回目の描画)
setIsLoading(true);
try {
//サーバーと通信
const res = await axios("http://127.0.0.1:3000/");
//State変更2:[]→res.data(*3回目の描画)
setUsers(res.data);
} catch (error) {
setIsError(true);
}
//State変更3:true→false(*4回目の描画)
setIsLoading(false);
};
fetchUserData();
},[]);
- 初回描画時
- カスタムフック内で
setIsLoading(true)
実行時 - カスタムフック内で
setUsers(res.data)
実行時 - カスタムフック内で
setIsLoading(false)
実行時
State
を変更した際に再描画されています。3はコンテクストで保持しているState
の更新ですが、コンテクストを参照しているコンポーネントは全て再描画されます。
後でTanStack Queryの場合は描画回数がどうなるのか注目してみましょう。
詳細
TanStack Queryを使った場合
ソース
TanStack Queryを使う場合はコンテクスト不要なのでカスタムフックから作成します。
カスタムフック
import { useQuery } from "@tanstack/react-query"
import axios from "axios"
//ユーザー情報をフェッチする関数
const getUsers = async () => {
const { data } = await axios.get("http://127.0.0.1:3000/")
return data
}
export const useQueryUsers = () => {
return useQuery({
//キャッシュを取得する時のキー
queryKey: ["users"],
//フェッチする関数
queryFn: getUsers,
//キャッシュを保持する時間
cacheTime: 10000,
//データが最新であるとみなす時間
staleTime: 0,
})
}
useState
やuseEffect
を使用することなくシンプルですね!
ユーザーをフェッチする関数(=getUsers()
)を定義して、キャッシュを取得する際に使用するqueryKey:"users"
を設定するだけって便利ですね・・・
ちなみにcacheTime,staleTime
は適当に設定していますが後で補足させていただきます。
画面コンポーネント
一覧
import { useQueryUsers } from "../hooks/useQueryFetchHook";
import { Link } from "react-router-dom";
const QueryFetch = () => {
//カスタムフックからデータとステータスを取得
const { status, data } = useQueryUsers();
console.log("QueryFetchが描画されました。");
//読み込み中
if (status === "loading") return <div>{"Loading"}</div>;
//エラー発生時
if (status === "error") return <div>{"Error"}</div>;
return (
<>
<h1>ユーザー一覧</h1>
<ul>
{data?.map((user) => {
return (
<li key={user.id}>
<Link to={`/query/detail?id=${user.id}`}>{user.name}</Link>
</li>
);
})}
</ul>
</>
);
};
export default QueryFetch;
カスタムフックから結果とステータスを受け取るだけです!
受け取り側もシンプルでいいですね。
詳細
import { useQueryClient } from "@tanstack/react-query";
import { useLocation } from "react-router-dom";
const QueryFetchDetail = () => {
console.log("QueryFetchDetailが描画されました。");
const queryClient = useQueryClient();
//キャッシュからユーザー情報を取得。queryKey:"users"を指定
const users = queryClient.getQueryData({ queryKey: ["users"] });
//URLパラメータからユーザーIDを取得し対象ユーザーを抽出する
const { search } = useLocation();
const urlParams = new URLSearchParams(search);
const id = Number(urlParams.get("id"));
const user = users.find((user) => user.id === id);
return (
<>
<h1>ユーザー詳細</h1>
<table>
<thead>
<tr>
<td>ID</td>
<td>名前</td>
<td>説明</td>
</tr>
</thead>
<tbody>
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.description}</td>
</tr>
</tbody>
</table>
</>
);
};
export default QueryFetchDetail;
一覧画面でキャッシュしたデータはqueryClient.getQueryData()
でquerykey:"users"
を指定して取得できます!
App.jsx
ルーティングとTanStack Queryの設定をします。
import ContextFetch from "./components/ContextFetch";
import UserDataContextProvider from "./Contexts/UserDataContext";
import ContextFetchDetail from "./components/ContextFetchDetail";
import { BrowserRouter, Route, Routes } from "react-router-dom";
+ import QueryFetch from "./components/QueryFetch";
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+ import QueryFetchDetail from "./components/QueryFetchDetail";
+ const queryClient = new QueryClient();
function App() {
return (
<BrowserRouter>
+ {/*TanStack QueryのProvider*/}
+ <QueryClientProvider client={queryClient}>
{/*コンテクストのProvider*/}
<UserDataContextProvider>
<Routes>
+ <Route path="/query" element={<QueryFetch />} />
+ <Route path="/query/detail" element={<QueryFetchDetail />} />
<Route path="/" element={<ContextFetch />} />
<Route path="/detail" element={<ContextFetchDetail />} />
</Routes>
</UserDataContextProvider>
+ </QueryClientProvider>
</BrowserRouter>
);
}
export default App;
queryClinet
を設定して、Provider
で各コンポーネントに提供しています。
queryClient
をnew
するときに引数で設定を渡すことも可能です。
これで実装完了です!動作確認していきましょう!
動作確認
一覧
無事表示されましたね!TanStack Queryでは描画回数はどうでしょうか・・・?
2回!先ほどよりも減っていますね!
この2回は初期描画と、TanStack Queryがサーバーからデータをフェッチした際に描画した2回となります。
コンテクストの時にState管理していたisLoading,isError
が不要になったことで描画回数が減った訳ですね。
詳細
補足(cacheTimeとstaleTimeについて)
先ほど、後回しにしたcacheTimeとstaleTimeの説明をさせてください。
- cacheTimeはデータをキャッシュする時間。
- staleTimeはキャッシュしたデータが古くなったとみなす時間
例えばcacheTimeを3000,staleTimeを0とした場合、データをフェッチしその1秒後に再フェッチしようとするとキャッシュされているためサーバーへのリクエストなしで表示できます。
ただ、staleTimeが0なのでキャッシュを返した後に自動でフェッチし、データが新しくなっている場合は最新の値に書き換えられます。
設定ではInfinity
(=永続的)も可能であり、常にキャッシュしたデータを保持及び最新と見なすことも可能です。
TanStack Queryの利点再確認
最初に利点があるといった箇所を振り返ってみましょう。
- 描画回数の最適化
→コンテクストを使った場合よりも再描画回数が減りましたね!これはTanStack QueryがLoading
やError
のState
を提供してくれることで軽減されました! - コンテクストを使用する場合よりも、コードがよりシンプルになる
→カスタムフックでuseState
やuseEffect
を使用することなくすっきりしましたね!
またTanStack Queryがデータをキャッシュ管理してくれるおかげでコンテクストも不要になり、よりシンプルになりました。 - サーバーとの通信処理が楽、キャッシュの管理も自動で実施
→関数を定義したらTanStack Query側でフェッチしてくれるのでいいですね!後は補足で述べたように、cacheTime ,staleTimeをお好みで設定してデータの鮮度をニーズに応じてコントロールできるのも強みだと感じました。
おわりに
TanStack Queryはいかがでしょうか?サーバー側と通信する場合は、役立つ可能性が強く採用してみるのもいいかと思います!
今回は取得だけでしたが、更新系の処理がある場合も記事を書いていきたいと思います!
最後までご覧いただきありがとうございました!