TanStack Query（旧React Query）を使ってみた

## はじめに

皆さん、[TanStack Query（旧React Query）](https://tanstack.com/query/latest)をご存知ですか？
TanStack Queryはデータフェッチ、データキャッシュを可能にする素敵なライブラリです。
今回は皆さんに紹介したく記事を書きました。

## TanStack Queryの利点

使用する前にざっくりと調べてみると、[React標準のコンテクスト](https://react.dev/learn/passing-data-deeply-with-context)を使用した場合と比較して、下記利点を説明している方が多かったです！

- 描画回数の最適化
- コードがよりシンプルになる
- サーバーとの通信処理が楽、キャッシュの管理も自動で実施

何やら良さげな雰囲気がしますね・・・！
コンテクストを使った場合と比較して良さを確認していきたいと思います！

## 使用した環境

- 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](https://expressjs.com/ja/)を使って作成します。

```shell:実行コマンド
npm install express cors 
```

リクエストを受けたらユーザーを返すプログラムを作成します。

```javascript:server.js
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);
```

実行して動作確認しておきましょう。

```shell:起動コマンド
node ./server.js
```
![image-20230422204609486.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/c72bb199-9752-176f-6dda-e1bd371616c6.png)


取得できていますね！次はクライアント側の準備を進めていきます。

### クライアント側の準備

[vite](https://ja.vitejs.dev/)を使ってサクッとプロジェクトを作成します。

```shell:実行コマンド
npm create vite@latest
```

プロジェクトを作成したらプロジェクト階層に移動して必要なライブラリをインストールします。
今回は`tanstack query`とHTTPライブラリの`axios`、擬似的な画面遷移を行いたいので`react-router-dom`を入れておきます。

```shell:実行コマンド
npm install @tanstack/react-query axios react-router-dom
```

## コンテクストを使った場合
まずはコンテクストを使用した場合を見ていきましょう。
### ソース

#### コンテクスト

サーバーから取得したユーザー情報を管理する`State,Context`を作成します。

```jsx:UserDataContext.jsx
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回発生します。

```javascript:useContextFetchHook.js
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`の場合はそれぞれメッセージを返します。

```jsx:ContextFetch.jsx
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;
```

##### 詳細

詳細画面では一覧画面表示時に取得したユーザー情報をコンテクストから取得します。

```jsx:ContextFetchDetail.jsx
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`を設定します。

```jsx:App.jsx
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;
```

これで実装完了です！動作確認して挙動を見ていきましょう。

### 動作確認

下記コマンドで実行します。

```shell:実行コマンド
npm run dev
```

#### 一覧
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/575fb7b6-8a87-6785-bf23-f065563e103f.png)

ちゃんと表示されていますね！ちなみに一覧画面は何回描画されたのでしょう・・・？
コンソールを確認してみます。

![スクリーンショット 2023-04-22 20.04.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/db66985f-f83b-961e-d306-f4d8c75024fd.png)
4回描画されていますね！実は初回描画以外は下記処理実行時に行われています。

```javascript:カスタムフック内のuseEffect
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();
},[]);
```

1. 初回描画時
2. カスタムフック内で`setIsLoading(true)`実行時
3. カスタムフック内で`setUsers(res.data)`実行時
4. カスタムフック内で`setIsLoading(false)`実行時

`State`を変更した際に再描画されています。3はコンテクストで保持している`State`の更新ですが、コンテクストを参照しているコンポーネントは全て再描画されます。
後でTanStack Queryの場合は描画回数がどうなるのか注目してみましょう。

#### 詳細

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/2f0bb992-6294-f590-1f29-f8bc68967fbb.png)
コンテクストから取得した情報が表示されていますね！

## TanStack Queryを使った場合

### ソース

TanStack Queryを使う場合はコンテクスト不要なのでカスタムフックから作成します。

#### カスタムフック

```javascript:useQueryFetchHook.js
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`は適当に設定していますが[後で補足](https://qiita.com/nanndot/private/1102bc784934fcd8ac5d#%E8%A3%9C%E8%B6%B3cachetime%E3%81%A8staletime%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)させていただきます。

#### 画面コンポーネント

##### 一覧

```jsx:QueryFetch.jsx
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;
```

カスタムフックから結果とステータスを受け取るだけです！
受け取り側もシンプルでいいですね。

##### 詳細

```jsx:QueryFetchDetail.jsx
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の設定をします。

```diff_jsx:App.jsx
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`するときに引数で設定を渡すことも可能です。

これで実装完了です！動作確認していきましょう！

###  動作確認

#### 一覧

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/58a667b5-c1f9-6a44-5154-22ae59f39e3d.png)

無事表示されましたね！TanStack Queryでは描画回数はどうでしょうか・・・？

![スクリーンショット 2023-04-22 20.29.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/1df5635a-8c62-900d-4a16-181c55b17be2.png)

2回！先ほどよりも減っていますね！
この2回は初期描画と、TanStack Queryがサーバーからデータをフェッチした際に描画した2回となります。
コンテクストの時にState管理していた`isLoading,isError`が不要になったことで描画回数が減った訳ですね。

#### 詳細

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/7c6de11b-ed89-d401-c695-da51fcff738c.png)
詳細画面もキャッシュしたデータを取得できていますね！

## 補足（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はいかがでしょうか？サーバー側と通信する場合は、役立つ可能性が強く採用してみるのもいいかと思います！

今回は取得だけでしたが、更新系の処理がある場合も記事を書いていきたいと思います！
最後までご覧いただきありがとうございました！