本文書ではTanStack Query(旧React Query)のバージョン5を使ってfetch関数やaxiosライブラリとの違いや初めての人だと分かりにくいstaleTimeやgcTimeの違い、Devtoolsの設定/確認方法などTanStack Queryを初めて利用する人にもわかるように動作確認を通して基本的な機能の説明を行っています。

バックエンドにはNode.jsのExpress, フロントエンドにはReactを利用しています。

TanStack Queryのバージョン4を利用したTodoアプリのCRUD設定については下記の記事で公開しています。

TanStack Queryとは

TanStack Queryはバージョン3まではReact Queryと呼ばれていましたがバージョン4になり名称がTanStack Queryになりました。以前までのバージョンで利用されてきたReact Queryは名前にReactが含まれている通りReact専用のライブラリでした。バージョンアップしてTanStack QueryになりReactだけではなくSolid, Vue, Svelteなどでも利用できるようになりました。

TanStack Queryを簡単に説明するとサーバの状態(データ)をfetching, caching, synchronizing and updating(サーバ上のデータをフェッチ、キャッシュ、同期、更新)することができるdata fetchingライブラリです。

バックエンドサーバの設定

バックエンドサーバにはExpress、データベースにはSQLiteを利用します。ExpressからSQLiteを利用するためにPrismaを利用しています。データの作成、更新、削除に利用できるバックエンドの環境が必要になることに備えてバックエンドサーバの構築を行っています。

Expressサーバの設定

Expressサーバをインストールするためにbackendフォルダを作成します。backendフォルダを作成後、backendフォルダに移動してnpm init -yコマンドを実行してpackage.jsonファイルの作成を行います。


 % mkdir backend
 % cd backend
 % npm init -y

expressとnodemonのパッケージのインストールを行います。nodemonをインストールすることでexpressサーバの処理を記述するファイルの更新を監視し更新が行われると自動で再読み込みを行ってくれます。


 % npm install express nodemon

backendフォルダにindex.jsファイルを作成します。


 % touch index.js

index.jsファイルにexpressの処理を記述していくためpackage.jsonファイルを開いてscriptsの箇所にnodemonの設定を行います。npm startコマンドを実行すると”nodemon index.js”が実行できます。


{
//略
  "scripts": {
    "start": "nodemon index.js"
  },
//略
}

作成済みのindex.jsファイルに以下のコードを記述します。Expressサーバの起動ポートは3000に設定しています。


const express = require('express');

const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

index.jsが更新できたらnpm startコマンドを実行します。Expressサーバがポート番号3000で起動します。


% npm start

> backend@1.0.0 start
> nodemon index.js

[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node index.js`
Example app listening on port 3000!

正常に動作しているかブラウザを利用してアクセスします。http://localhost:3000にアクセスして”Hello World”が表示されれば問題なく起動しています。

Prismaの設定

Prismaを利用するとExpressサーバからデータベースへの接続はPrismaを経由して行うことになります。PrismaはORM(Object Relational Mapping)ツールでオブジェクトとデータベースをマッピングできるためSQLではなくオブジェクトのメソッドを利用してデータベースを操作することができます。またデータベースとサーバとの間に入ることでデータベースの違いを吸収してくれるためデータベースを変更しても同じオブジェクトのメソッドを利用することができます。

ExpressからSQLiteデータベースを操作するためにはPrismaは必須ではありません。

Prismaを利用するためにnpmコマンドでインストールを行います。


 % npm install prisma

インストール後にPrismaの設定ファイルを作成するために初期化コマンド(npx prisma init)を実行します。接続するデータベースがSQLiteなのでオプションの–datasource-provider sqliteをつけて実行します。


 % npx prisma init --datasource-provider sqlite

コマンドを実行するとprismaフォルダと.envファイルが作成されます。prismaフォルダにはPrismaの設定ファイルschema.prismaが作成されます。schema.prismaファイルにデータベースへの接続情報やデータモデルを記述します。データモデルはテーブルの構成情報です。


// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

SQLiteの設定になっているので上記の設定はそのままでデータモデルを記述します。データモデルの記述はデータベースによって異なるのでテーブルに追加した列の型の設定方法やリレーションシップなど不明な場合はPrismaのドキュメントを参考に設定を行ってください。

ここではTodoテーブルを作成するので以下を記述します。Todoテーブルはid, name, isCompletedの3つの列を持ち、idは自動で順番に番号が付与されます。nameはTodoの名前で文字列、isCompletedはTodoが完了したかどうかを表し、trueやfalseのboolean値のみ設定できます。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Todo {
  id      Int      @id @default(autoincrement())
  name   String
  isCompleted    Boolean
}

schema.prismaの設定が完了したらデータベースファイル(SQLiteはファイルベースのデータベース)とテーブルを作成するためnpx prisma db pushコマンドを実行します。コマンドを実行するとprismaフォルダにdev.dbファイルが作成されます。


 % npx prisma db push

テーブルが作成できているかどうかはPrisma Studioを利用することができます。SQLiteのデータベースへの接続はコマンドによる接続やデータベースの管理ソフト(例:TablePlus)によっても行うことができます。


 % npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555

Prisma Studioからはデータの閲覧だけではなくデータの追加もできるので2件のデータ追加を行います。Prisma Studioからはデータの追加だけではなく削除や更新も可能です。

Prisma Studioからデータを追加
Prisma Studioからデータを追加

ここまででPrismaの設定は初期データの作成は完了です。

データの取得

Prismaの設定が完了してデータベースのテーブルにデータを追加したので追加したデータをExpressサーバから取得します。index.jsファイルではimportしたPrismaClientを利用してデータベースへのアクセスを行います。prismaインスタンスにテーブル名を指定してfindManyメソッドを実行することでtodoテーブルにあるすべてのデータを取得することができます。


const express = require('express');
const { PrismaClient } = require('@prisma/client');

const app = express();
const port = 3000;

const prisma = new PrismaClient();
app.get('/todos', async (req, res) => {
  const todos = await prisma.todo.findMany();
  return res.json(todos);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

ブラウザからhttp://localhost:3000/todosにアクセスするとテーブルから取得したデータがJSONで表示されます。

データベースから取得したデータの表示
データベースから取得したデータの表示

Reactの設定

プロジェクトの作成

npm create vite@latestコマンドを実行してVite環境でReactプロジェクトを作成します。コマンドを実行するとプロジェクト名の設定を行う必要がありますが任意の名前をつけることができるのでここではfrontendとしています。frameworkはReact, variantではJavaScriptを選択しています。


 % npm create vite@latest
✔ Project name: … frontend
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Scaffolding project in /Users/mac/Desktop/tanstack_query/frontend...

Done. Now run:

  cd frontend
  npm install
  npm run dev

コマンドを実行後にfrontendに移動してnpm installコマンドを実行します。


 % npm install

TanStack Queryのインストール

TanStack Queryのインストールはnpmコマンドで行います。


 % npm i @tanstack/react-query

インストール後にreactとtanstack/react-queryのバージョンをpackage.jsonファイルで確認します。


{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tanstack/react-query": "^5.8.9",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.37",
    "@types/react-dom": "^18.2.15",
    "@vitejs/plugin-react": "^4.2.0",
    "eslint": "^8.53.0",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.4",
    "vite": "^5.0.0"
  }
}

Reactは18.2.0で、tanstack/react-queryは5.8.9であることがわかります。このバージョンを利用して動作確認を行います。

Todoコンポーネントの作成

srcフォルダの下にcomponentsフォルダを作成してTodo.jsxファイルを作成します。


const Todo = () => {
  return (
    <>
      <h1>Todo一覧</h1>
    </>
  );
};

export default Todo;

作成したTodoコンポーネントは App.jsファイルでimportを行います。


import Todo from './components/Todo';
import './App.css';

function App() {
  return (
    <div className="App">
      <Todo />
    </div>
  );
}

export default App;

デフォルトで適用されているスタイルを解除するためにmain.jsxファイルのindex.cssファイルのimportをコメントしておきます。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
// import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

開発サーバはnpm run devコマンドで起動します。


 % npm run dev

> frontend@0.0.0 dev
> vite


  VITE v5.0.3  ready in 251 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

ブラウザからアクセスすると画面にはTodo一覧の文字列が表示されます。

Todo一覧の文字列の表示
Todo一覧の文字列の表示

TanStack Queryの基本設定

データの取得

TodoコンポーネントからTanStack Queryを利用してExpressサーバからTodoのデータを取得します。データ取得に利用するuseQuery Hookを利用したコードは下記の通りです。


import { useQuery } from '@tanstack/react-query';

const fetchTodos = async () => {
  const res = await fetch('http://localhost:3000/todos');
  return res.json();
};

const Todo = () => {
  const { data: todos } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  return (
    <>
      <h1>Todo一覧</h1>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>{todo.name}</li>
        ))}
      </ul>
    </>
  );
};

export default Todo;

TanStack QueryではimportしたuseQuery Hookを利用してサーバへのQueryを実行します。useQueryの引数にはオブジェクトを利用してqueryKeyとqueryFnの設定を行います。queryKeyにはユニークなキーでアプリケーションを通してキャッシュやQueryの共有に利用されます。queryFnにはPromiseを戻す関数を設定します。queryKey, qeuryFn以外にもオプションがありますがこの2つが設定に必須です。本文書の中でもいくつかオプションを利用しますがその他のオプションについてはドキュメントのuseQueryのAPIで確認することができます。useQueryから戻されるdataには別名のtodosをつけていますがそのままdataを利用することもできます。

fetch関数ではなくaxiosライブラリを利用した場合については後ほど説明を行います。

useQueryによるデータ取得の動作確認を行うためブラウザからアクセスするとブラウザのコンソールには以下のエラーメッセージが表示されます。


QueryClientProvider.tsx:18 Uncaught Error: No QueryClient set, use QueryClientProvider to set one

useQueryを利用するためにはここまでの設定では不足しており、メッセージの説明からQueryClientとQueryClientProviderの設定が必要であることがわかります。

main.jsxファイルでQueryClientProvider、QueryClientの設定を行います。TodoコンポーネントでuseQueryを利用するためにはルートコンポーネントのAppをQueryClientProviderでwrapする必要があります。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// import './index.css'

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

再度ブラウザからアクセスするとTodo一覧の文字列のみ表示されます。これはTanStack Query側の問題ではなくブラウザのコンソールを見るとCORSのエラーが発生しているのでサーバ側でCORSの設定を行う必要があります。


Access to fetch at 'http://localhost:3000/todos' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

CORSはCross Origin Resource Sharing(オリジナ間リソース共有)の略でReactの起動ポートとExressサーバの起動ポートが異なるため異なるオリジンとみなされ接続することができません。CORSを設定することで異なるオリジンでの通信が可能となります。

backendフォルダでcorsのインストールを行います。Reactのプロジェクト側のフォルダではないので注意してください。


 % npm install cors

インストールが完了したらindex.jsファイルにcorsの設定を追加します。


const express = require('express');
const cors = require('cors');
const { PrismaClient } = require('@prisma/client');

const app = express();
const port = 3000;

app.use(cors());

const prisma = new PrismaClient();
app.get('/todos', async (req, res) => {
  const todos = await prisma.todo.findMany();
  return res.json(todos);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

サーバ側でのCORSの設定完了後、ブラウザからアクセスするとuseQueryを利用してExpressサーバから取得したデータがブラウザ上に表示されます。

useQueryを利用して取得したデータの表示
useQueryを利用して取得したデータの表示

ここまでの設定でTanStackを利用してバックエンドサーバから取得したデータをブラウザ上に表示できるようになりました。

useQueryの戻り値の確認

バックエンドサーバからデータを取得するだけならTanStack Queryを利用しなくてもfetch関数、axiosライブラリ単体でも行うことができます。useQueryはfetch関数やaxiosとどのような違いがあるのか戻り値を利用して確認します。

useQueryの戻り値のオブジェクトの中からdataだけを利用していましたがQueryに関するさまざまな情報が戻されます。どのようなデータが戻されるのかはTansStack Queryのドキュメントで確認することができます。

useQuery  APIで戻り値を確認
useQuery APIで戻り値を確認

戻されるデータの中からisPending, isError, errorを使った場合の利用方法を確認します。isPendingはQueryがまだデータを持っていない状態を表し, isErrorはエラーが発生した状態を表しています。どちらもbooleanの値を持ちます。errorについてはエラーがthrowされた場合にオブジェクトとしてエラーの情報を含みます。

isPending, isError, errorを利用する場合は以下のようにコードを記述することができます。isPendingやisErrorの値によってユーザに表示させる内容を変更することができます。


import { useQuery } from '@tanstack/react-query';

const fetchTodos = async () => {
  const res = await fetch('http://localhost:3000/todos');
  return res.json();
};

const Todo = () => {
  const {
    isPending,
    isError,
    data: todos,
    error,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  if (isPending) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <>
      <h1>Todo一覧</h1>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>{todo.name}</li>
        ))}
      </ul>
    </>
  );
};

export default Todo;

useQueryによるデータの取得中にはブラウザ中に”isLoading”が表示されますが一瞬です。本当に”isLoading”が表示されているのか確認するためにバックエンド側でリクエストに対するレスポンスを戻すまでの待ちを追加します。


app.get('/todos', async (req, res) => {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const todos = await prisma.todo.findMany();
  return res.json(todos);
});

3秒間の待ちを追加したのでデータが戻されるまでブラウザ上には”Loading…”の文字列が表示されます。

データ取得中に表示されるLoading
データ取得中に表示されるLoading

エラーが発生した場合の動作確認も行いたいのでExpreeサーバ側で意図的にステータスコード500を戻すように設定を行います。


app.get('/todos', async (req, res) => {
  //   await new Promise((resolve) => setTimeout(resolve, 3000));
  //   const todos = await prisma.todo.findMany();
  //     return res.json(todos);
  return res.status(500).json({ message: 'Internal Server Error' });
});

Expressサーバからエラーが戻されていますがブラウザ上には変化はありません。ブラウザのコンソールを見ると”Uncaught TypeError: todos?.map is not a function”が表示されています。

Todo.jsxファイルのQuery実行後のtodosの中身をconsole.logで確認するとサーバから戻されたエラーがオブジェクトとしてそのまま入っているためmap関数を利用して展開することができないため”Uncaught TypeError: todos?.map is not a function”のエラーとなっています。


"{message: 'Internal Server Error'}"

useQueryのqueryFnで指定したfetchTodos関数ではfetch関数を利用しているのでリクエストが成功したかどうかをfetch関数実行後に戻されるresponseオブジェクトのokプロパティ(response.ok)で確認する必要があります。fetchでは400, 500などのステータスコードが戻されたとしてもそのまま処理を行うためresponse.okで確認を行うことでエラーを検知させます。ステータスコードが200の場合はresponse.okはtrueになりますが400や500ではresponse.okはfalseとなります。


const fetchTodos = async () => {
  const res = await fetch('http://localhost:3000/todos');
  if (!res.ok) {
    throw new Error(`${res.status} ${res.statusText}`);
  }
  return res.json();
};

res.okの分岐を入れたことでステータスコード500が戻され場合はres.okがfalseとなりエラーがthrowされます。エラーがthrowされたことによりisErrorがtrueとなりブラウザ上にはエラーメッセージが表示されます。

エラーメッセージの表示
エラーメッセージの表示

エラーが表示されることが確認できましたがLoaing画面が表示されてからエラーメッセージが表示されるまで少し時間がかかります。useQueryではエラーが発生しても複数回自動でリトライを行います。リトライはオプションで設定することができデフォルトでは3に設定されています。リトライしてもエラーが発生した場合にエラーメッセージが画面に表示されます。

リトライ実行の確認
リトライ実行の確認

リトライ回数を増やしたい場合にはオプションのretryで設定を行うことができます。


const Todo = () => {
  const {
    isPending,
    isError,
    data: todos,
    error,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    retry: 10,
  });
//略

useQueryはfetchで利用した場合とは異なり、データだけではなくisLoading, isError, errorなどの情報を利用できることがわかりました。またリトライなどの機能を備えていることもわかります。

エラーの処理と表示内容について

表示されているエラーの内容をサーバ側で設定したメッセージにしたい場合はresponse.json()やresponse.text()を利用することができます。サーバが戻されるメッセージがサーバ側からだとわかるようにメッセージの内容を変更しておきます。


app.get('/todos', async (req, res) => {
  return res.status(500).json({ message: 'Internal Server Error From Server' });
});

res.okがfalseの場合もres.jsonを利用して戻されるデータを取得します。res.jsonから戻されるjsonにはサーバから戻されたJSONデータがオブジェクトに変換されて入っているためmessageプロパティでメッセージの内容にアクセスすることができます。


const fetchTodos = async () => {
  const res = await fetch('http://localhost:3000/todos');
  if (!res.ok) {
    const json = await res.json();
    throw new Error(json.message);
  }
  return res.json();
};

エラーが発生した場合、先程までのメッセージとは異なりサーバ上で設定したmessageの値が表示されるようになりました。

サーバから戻されたエラーを表示
サーバから戻されたエラーを表示

res.json()ではオブジェクトとしてデータを取り出すことができますがres.text()の場合はテキスト(JSONデータ)として取得することができます。


const fetchTodos = async () => {
  const res = await fetch('http://localhost:3000/todos');
  if (!res.ok) {
    const text = await res.text();
    throw new Error(text);
  }
  return res.json();
};
respnose.text()を利用した場合
respnose.text()を利用した場合

textは文字列なのでそのまま表示させると下記のように表示されます。

useQueryで戻されるerrorのmessageにはthrowしたres.text()がJSONデータとして保存されているのでJSON.parseを利用してオブジェクトに変換してmessageプロパティの内容を表示することも可能です。


if (isError) {
    const text = JSON.parse(error.message);
    return `Error: ${text.message}`;
}

下記のように表示されます。サーバから戻されるJSONデータが複雑な場合もJSON.parseでオブジェクトに変換することで必要な情報をブラウザ上に表示させることができます。

サーバから戻されたエラーを表示
サーバから戻されたエラーを表示

サーバから戻されるエラーの表示方法について確認することができました。

axiosを利用した場合

データを取得する際にfetch関数ではなくaxiosライブラリを利用することができます。axiosを利用した場合にはサーバからステータスコードの400や500が戻された場合にはエラーの処理を行ってくれるので違いを確認しておきます。

axiosを利用したい場合はaxiosのインストールが必要になります。Reactのプロジェクトでインストールを行います。


% npm install axios

fetchTodos関数を利用して取得したデータの表示関数をfetchからaxiosに変更します。エラーがない場合は下記のコードでユーザ一覧が表示されます。


import axios from 'axios';
import { useQuery } from '@tanstack/react-query';

const fetchTodos = async () => {
  const res = await axios.get('http://localhost:3000/todos');
  return res.data;
};

const Todo = () => {
  const {
    isPending,
    isError,
    data: todos,
    error,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });
//略

エラーが発生した場合にどのようなエラーが渡されるのか確認します。


if (isError) {
  console.log(error);
  return <span>Error: {error.message}</span>;
}

コンソールを確認するとAxiosErrorオブジェクトが表示されます。


{
    "message": "Request failed with status code 500",
    "name": "AxiosError",
    "stack": "AxiosError: Request failed with status code 500\n    at settle (http://localhost:3000/static/js/bundle.js:42760:12)\n    at XMLHttpRequest.onloadend (http://localhost:3000/static/js/bundle.js:41442:66)",
    "config": {
        "transitional": {
            "silentJSONParsing": true,
            "forcedJSONParsing": true,
            "clarifyTimeoutError": false
        },
        "adapter": [
            "xhr",
            "http"
        ],
        "transformRequest": [
            null
        ],
        "transformResponse": [
            null
        ],
        "timeout": 0,
        "xsrfCookieName": "XSRF-TOKEN",
        "xsrfHeaderName": "X-XSRF-TOKEN",
        "maxContentLength": -1,
        "maxBodyLength": -1,
        "env": {},
        "headers": {
            "Accept": "application/json, text/plain, */*"
        },
        "method": "get",
        "url": "http://localhost:3000/todos"
    },
    "code": "ERR_BAD_RESPONSE",
    "status": 500
}

ブラウザを確認するとAixosErrorオブジェクトのmessageプロパティの内容が表示されます。

AxiosErrorのmessageプロパティの内容が表示
AxiosErrorのmessageプロパティの内容が表示

axiosの場合はサーバから戻されるデータはresponseオブジェクトのdataに含まれているのでサーバからのメッセージを表示したい場合にはresponose.dataを利用することができます。


if (isError) {
  return <span>Error: {error.response.data.message}</span>;
}
サーバから戻されたエラーを表示
サーバから戻されたエラーを表示

TanStack Queryの機能の確認

TanStack Queryを利用することでデータ取得中にはisLoadingを表示させたりエラー処理を簡単に行えることがわかりました。しかしこれだけの機能だけではfetch関数やaxiosライブラリの機能との大きな違いはありません。ここではTanStack Queryが持つその他の機能について確認していきます。

Window Focus Refetching

Windows Focus Refetchingという機能がデフォルトでtrueに設定されており、一度アプリケーションを離れて戻ってくると自動でデータの再フェッチが行われます。

例えばブラウザ上の別タブに移動して再度アプリケーションの画面に戻ると再フェッチが行われます。実際に動作確認で確かめます。

現在閲覧しているタブの他に別のタブを開いておきます。ネットワークタブを見るとtodosへのリクエストを1件確認することができます。

ネットワークタブの確認
ネットワークタブの確認

一度別のタブに移動して戻ってきてください。再度ネットワークタブを確認すると再度todosへのリクエストが行われていることがわかります。タブ移動を繰り返すと繰り返した分だけリクエストが行われます。

再度ネットワークタブを確認
再度ネットワークタブを確認

この機能が有効になっているのではuseQueryのオプションのrefetchOnWindowFocusがデフォルトでtrueに設定されているためです。refetchOnWindowFocusの値をfalseにするとリフェッチは行われなくなります。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  refetchOnWindowFocus: false,
});

アプリケーションから離れている間にバックエンドサーバでデータが追加された場合でも戻ってくるとリフェッチにより最新のサーバ上のデータを自動で取得することができます。

リフェッチ設定

refetchOnWindowFocusがtrueの場合はタブを切り替えるとリフェッチが行えることがわかりました。タブを切り替えることなく一定間隔でリフェッチを行いたい場合にはrefetchIntervalを利用することができます。

refetchIntervalの値を1,000に設定すると1行間隔ごとにリフェッチが行われます。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  refetchInterval: 1000,
});

staleTimeの設定

staleTimeは取得したデータがstale(古い)された状態とみなすまでの時間設定でデフォルトでは0に設定さえています。つまり取得したデータは取得直後にstaleされた状態(古い状態)とみなされます。Window Focus Refetchingの機能は取得したデータがstaleの場合だけリフェッチを行うのでrefetchOnWindowFocusの値がデフォルト値のtrueの場合はタブを切り替えると必ずリフェッチが行われます。staleTimeを設定することでその時間内では取得したデータがstaleされた状態とみなされないためリフェッチが行われません。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000,
});

上記のようにstaleTimeを5秒に設定してタグの切り替えを行います。データを取得してから5秒間はどんなにタブを切り替えてもリフェッチは行われません。5秒経過してタブを切り替えるとリフェッチが行われます。

値をInfinityにするとデータはstaleされた状態とみなされなくなるためリフェッチは行われません。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: Infinity,
});

gcTimeの設定

gcTimeの先頭の文字gcはGarbage Collection(ガーベジコレクション)の略でInActive/UnUsedのキャッシュデータをメモリ上に残しておく時間です。デフォルトでは5分に設定されているのでその間に同じQueryが実行された場合はキャッシュされたデータを利用することができます。

ここではTodoコンポーネントを表示・非表示(マウント・アンマウント)と切り替えることでgcTimeの設定がアプリケーションにどのような影響を与えるか確認します。

Todoコンポーネントの表示・非表示を切り替えるが行えるようにApp.jsファイルでuseState Hookを利用した Toggleボタンを追加します。


import Todo from './components/Todo';
import './App.css';
import { useState } from 'react';

function App() {
  const [show, setShow] = useState(true);
  return (
    <>
      <button onClick={() => setShow(!show)}>Todo Toggle</button>
      <div className="App">{show && <Todo />}</div>
    </>
  );
}

export default App;

デフォルトでの動作確認

最初はuseQueryのオプションを設定しないまま動作を確認します。


import axios from 'axios';
import { useQuery } from '@tanstack/react-query';

const fetchTodos = async () => {
  const res = await axios.get('http://localhost:3000/todos');
  return res.data;
};

const Todo = () => {
  const {
    isPending,
    isError,
    data: todos,
    error,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  if (isPending) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <span>Error: {error.response.data.message}</span>;
  }

  return (
    <>
      <h1>Todo一覧</h1>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>{todo.name}</li>
        ))}
      </ul>
    </>
  );
};

export default Todo;

バックエンドサーバでは3秒間遅延を発生させておきます。


app.get('/todos', async (req, res) => {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const todos = await prisma.todo.findMany();
  return res.json(todos);
});

ページにアクセスした直後はデータがキャッシュにないため”Loading…”が表示されます。データ取得が完了すると以下の画面が表示されます。

Todoコンポーネントが表示されている状態
Todoコンポーネントが表示されている状態

“Todo Toggle”ボタンをクリックするとTodoコンポーネントは非表示になりますがネットワークタブに変化はありません。

Todoコンポーネントが非表示に状態
Todoコンポーネントが非表示に状態

再度”Todo Toggle”ボタンをクリックするとTodo一覧はキャッシュデータを利用しているので即座に表示されますがネットワークタブを見るとバックエンドサーバへのリクエストが行われていることが確認できます。3秒間の遅延があるのでStatusには”Pending”と表示されています。

ネットワークリクエストの送信確認
ネットワークリクエストの送信確認

gcTimeを0に設定した場合

useQueryのオプションの設定でgcTimeを0にします。gcTimeは0なのでキャッシュデータはすぐにgarbage collectionによって破棄されます。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  gcTime: 0,
})

Todoコンポーネントを非表示するまでの動作は先程と違いはありませんが、非表示の状態から”Todo Toggle”ボタンをクリックすると再度画面には”Loading…”の文字が表示され、バックエンドのサーバからのデータが戻されるまで”Loading…”の表示された状態になります。

Lodingの文字列が表示
Lodingの文字列が表示

つまりgcTimeの設定が0のためキャッシュのデータが破棄され存在しないためデータを表示することができず、バックエンドからデータを取得するまで”Loading…”の文字が表示されることになります。

staleTimeの設定した場合

gcTimeが0の場合はキャッシュデータがないので必ずバックエンドサーバへのリクエストが送信されます。デフォルトの場合でもgcTimeが5分、staleTimeが0なので取得したデータも即座にstaleな状態となり、データはブラウザ上に表示されると同時にリクエストも送信されます。staleTimeを設定することで指定した時間内であればデータはstaleな状態ではないためコンポーネントを非表示から表示に切り替えた場合にリクエストを送信しないということも可能です。

下記ではstaleTimeを3000(3秒)に設定しているのでページにアクセス後データを取得してから3秒間は非表示・表示を切り替えてもリクエストが送信されることはありません。その間はキャッシュにあるデータが表示されます。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 3000,
})

Devtoolsの設定

TanStack Query専用のDevtoolsが用意されているのでDevtoolsを確認することでTanStack Query内で管理しているデータのステータスを確認することができます。

インストールはnpmコマンドで行います。


 % npm i @tanstack/react-query-devtools

インストール後はアプリケーション全体でDevtoolsが利用できるようにmain.jsxファイルでReactQueryDevtoolsタグ設定を行います。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// import './index.css'

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </React.StrictMode>
);

設定完了後にブラウザから確認すると画面右下にアイコンが追加されます。

DevTools設定後に表示されるアイコン
DevTools設定後に表示されるアイコン

アイコンをクリックすると以下の画面が表示されます。Fresh, Fetching, Paused, Stale, Inaciveなどの状態を確認することができます。

DevToolsの表示
DevToolsの表示

デフォルトでの動作確認

オプションを何も設定していない状態でどのようにステータスが変化するのか確認します。

ページにアクセスするとデータの取得を行なっているのでFetchingの値が1になります。

データ取得中のステータス
データ取得中のステータス

データの取得が完了するとisFetchingの値が1となり、Staleの状態が1となります。デフォルトではstaleTimeが0に設定されているのですぐにstaleとなることと一致します。

データ取得後の状態
データ取得後の状態

“Todo Toggle”ボタンをクリックするとTodo一覧は非表示となりデータは利用されていないのでInactiveが1になります。

Todoコンポーネントが非表示の状態
Todoコンポーネントが非表示の状態

再度Todoコンポーネントを表示させるため”Todo Toggle”ボタンをクリックします。データのリフェッチが行われるのでFetchingの値は1になり、その他の値は0になります。

Todoコンポーネントを再表示
Todoコンポーネントを再表示

リフェッチによるデータの取得後はStaleの値が1になります。

staleTime設定した場合

staleTimeを設定した場合にDevtoolsのステータスがどのように表示されるか確認します。staleTimeを2,000(2秒)に設定しています。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 2000,
});

設定後、ページにアクセスしてデータの取得後が完了するとFreshの値が1となります。2秒経過するとFreshの値は0となり、Staleの値が1となります。データはstaleな状態ではないのでこの間はrefetchOnWindowFocusがtrueでもタブの切り替えを行ってもリフェッチされることはありません。

staleTimeを設定した場合
staleTimeを設定した場合

gcTimeを設定した場合

gcTimeを0に設定した場合にどのように表示されるかを確認します。


const {
  isPending,
  isError,
  data: todos,
  error,
} = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  gcTime: 0,
});

ページにアクセスした時に違いはなくデータ取得中はisFetchingは1でデータ取得後はstaleが1となります。”Toggle Button”をクリックするとすべての状態の値は0となりuseQueryで設定したqueryKeyの[“todos”]が消えます。つまりキャッシュデータが存在しないことを表しています。

gcTimeを0に設定した場合
gcTimeを0に設定した場合

gcTimeを5,000(5秒)に設定した場合は”Todo Toggle”ボタンをクリックしてから5秒後に[“todos”]が消えます。

複数のuseQueryの設定

複数のuseQueryが設定されている場合にはどのように表示されるか確認するためにcomponentsディレクトリにUser.jsxファイルを作成します。データはJSONPlaceHolderから取得しています。useQueryのqueryKeyには異なるキーを設定しています。


import axios from 'axios';
import { useQuery } from '@tanstack/react-query';

const fetchUsers = async () => {
  const res = await axios.get('https://jsonplaceholder.typicode.com/users');
  return res.data;
};

const User = () => {
  const {
    isPending,
    isError,
    data: users,
    error,
  } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  if (isPending) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <span>Error: {error.response.data.message}</span>;
  }

  return (
    <>
      <h1>Users一覧</h1>
      <ul>
        {users?.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
};

export default User;

App.jsxファイルで作成したUserコンポーネントをimportします。


import User from './components/User';
import Todo from './components/Todo';
import './App.css';
import { useState } from 'react';

function App() {
  const [show, setShow] = useState(true);
  return (
    <>
      <button onClick={() => setShow(!show)}>Todo Toggle</button>
      <div className="App">{show && <Todo />}</div>
      <div>
        <User />
      </div>
    </>
  );
}

export default App;

ページにアクセスするとそれぞれのリクエストの状態が表示されていることが確認できます。

2つのリクエストを送信した場合
2つのリクエストを送信した場合

DevToolsを利用することでリクエストのステータスを簡単に確認できることがわかりました。

その他にもTanStack Queryにもさまざまな機能があるので興味を持った方はぜひ他の機能も確認してみてください。