この記事で分かること
- React Queryでのデータ取得方法
- React Queryを使用した場合と使用しない場合の違い
React Queryとは
- TanStack QueryのReactアダプター
- TanStack QueryとReactフレームワークの間でデータの取り扱いや通信を調整する
- データの取得、キャッシュ、更新の仕組みを提供する
- サーバーステートの管理ライブラリであると説明されている
React Queryが提供する機能
- ローディング
- エラー
- ページング
- 無限スクロール
- プリフェッチ
- データの更新
- 重複排除(De-duplication)
- リトライ
- コールバック
利用手順
- パッケージのインストール
- package.jsonの有るディレクトリで以下を実行
npm install @tanstack/react-query
- package.jsonの有るディレクトリで以下を実行
- 親コンポーネント(今回はindex.js)でクエリクライアントの作成
- クエリクライアントはクエリとキャッシュを管理する
- 親コンポーネント(今回はindex.js)でクエリプロバイダを設定
- Propsとしてクエリクライアント渡す
- 子コンポーネント(今回はApp.js)で
useQuery
を呼び出す -
userQuery
の引数としてqueryKeyとqueryFnが必須 -
queryKey
はクエリの実行結果を一意に特定し、キャッシュに利用される(queryKeyが同じならAPIを叩かずにuseQueryのキャッシュデータが利用される) -
queryFn
は実際にAPIを叩く関数を指定する
サンプルコード
全ソースコードはこちら
機能概要
- PokeAPIにアクセスし、ポケモン情報を取得する
- 1ページあたり20体のポケモンを表示する
- Prevボタンで前の20体を表示する
- Nextボタンで次の20体を表示する
実行結果
フォルダ構成(抜粋)
ProjectRoot
└─ src
├─ components
│ └─ Card.js
├─ utils
│ └─ pokemon.js
├─ App.js
└─ index.js
src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
src/App.js
import { useState } from "react";
import "./App.css";
import { getAllPokemon, getPokemon } from "./utils/pokemon";
import Card from "./components/Card";
import { useQuery } from "@tanstack/react-query";
function App() {
const initialURL = "https://pokeapi.co/api/v2/pokemon/";
const [offset, setOffset] = useState(0);
// 前ページ
const handlePrevPage = () => {
if (offset === 0) {
return;
}
setOffset((prev) => prev - 20);
};
// 次ページ
const handleNextPage = () => {
setOffset((prev) => prev + 20);
};
// ポケモン一覧を取得するuseQuery
const { data: pokemonList, isLoading: isLoading1 } = useQuery({
queryKey: ["pokemonList", offset],
queryFn: () => getAllPokemon(initialURL, offset),
});
// ポケモンの詳細データ取得ラッパー
const loadPokemon = async (data) => {
let _pokemonData = await Promise.all(
data.map((pokemon) => {
let pokemonRecord = getPokemon(pokemon.url);
return pokemonRecord;
})
);
return _pokemonData;
};
// ポケモンの詳細データを取得するuseQuery
const { data: pokemonData, isLoading: isLoading2 } = useQuery({
queryKey: ["pokemonData", pokemonList],
queryFn: () => loadPokemon(pokemonList.results),
enabled: !!pokemonList, // ポケモン一覧取得が完了している場合に動作させる
});
// ポケモンデータの表示
return (
<div className="App">
{isLoading1 || isLoading2 ? (
<h1>Loading...</h1>
) : (
<>
{/* 各ポケモンの表示 */}
<div className="pokemonCardContainer">
{pokemonData.map((pokemon, i) => {
return <Card key={i} pokemon={pokemon} />;
})}
</div>
{/* PrevボタンとNextボタン */}
<div style={{ margin: "50px" }}>
<button onClick={handlePrevPage}>Prev</button>
<button onClick={handleNextPage}>Next</button>
</div>
</>
)}
</div>
);
}
export default App;
src/components/Card.js
import React from "react";
const Card = ({ pokemon }) => {
return (
<div className="card" style={{ marginTop: "50px" }}>
<div className="cardImg">
<img src={pokemon.sprites.front_default} alt="" />
</div>
name: {pokemon.name}
<br />
weight: {pokemon.weight}
</div>
);
};
export default Card;
src/utils/pokemon.js
// ポケモン一覧の取得
export const getAllPokemon = async (url, offset = 0) => {
try {
const response = await fetch(`${url}?offset=${offset}&limit=20`);
const data = await response.json();
return data;
} catch (error) {
throw error;
}
};
// ポケモンの詳細データ取得
export const getPokemon = async (url) => {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
throw error;
}
};
React Queryを使わない場合
src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src/App.js
import { useEffect, useState } from "react";
import "./App.css";
import { getAllPokemon, getPokemon } from "./utils/pokemon";
import Card from "./components/Card/Card";
function App() {
const initialURL = "https://pokeapi.co/api/v2/pokemon/";
const [loading, setLoading] = useState(true);
const [pokemonData, setPokemonData] = useState([]);
const [prevUrl, setPrevUrl] = useState("");
const [nextUrl, setNextUrl] = useState("");
useEffect(() => {
const fetchPokemonData = async () => {
// ポケモン一覧取得
let res = await getAllPokemon(initialURL);
// 詳細データ取得
loadPokemon(res.results);
// 前ページと次ページのURLをセット
setPrevUrl(res.previous);
setNextUrl(res.next);
// ローディング終了
setLoading(false);
};
fetchPokemonData();
}, []);
// ポケモンの詳細データ取得ラッパー
const loadPokemon = async (data) => {
let _pokemonData = await Promise.all(
data.map((pokemon) => {
let pokemonRecord = getPokemon(pokemon.url);
return pokemonRecord;
})
);
setPokemonData(_pokemonData);
};
// 前ページ
const handlePrevPage = async () => {
if (!prevUrl) return;
setLoading(true);
let data = await getAllPokemon(prevUrl);
await loadPokemon(data.results);
setPrevUrl(data.previous);
setNextUrl(data.next);
setLoading(false);
};
// 次ページ
const handleNextPage = async () => {
setLoading(true);
let data = await getAllPokemon(nextUrl);
await loadPokemon(data.results);
setPrevUrl(data.previous);
setNextUrl(data.next);
setLoading(false);
};
// ポケモンデータの表示
return (
<div className="App">
{loading ? (
<h1>Loading...</h1>
) : (
<>
{/* 各ポケモンの表示 */}
<div className="pokemonCardContainer">
{pokemonData.map((pokemon, i) => {
return <Card key={i} pokemon={pokemon} />;
})}
</div>
{/* PrevボタンとNextボタン */}
<div style={{ margin: "50px" }}>
<button onClick={handlePrevPage}>Prev</button>
<button onClick={handleNextPage}>Next</button>
</div>
</>
)}
</div>
);
}
export default App;
src/components/Card.js
※React Queryを使う場合とコードの内容は同じ
import React from "react";
const Card = ({ pokemon }) => {
return (
<div className="card" style={{ marginTop: "50px" }}>
<div className="cardImg">
<img src={pokemon.sprites.front_default} alt="" />
</div>
name: {pokemon.name}
<br />
weight: {pokemon.weight}
</div>
);
};
export default Card;
src/utils/pokemon.js
// ポケモン一覧の取得
export const getAllPokemon = async (url) => {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
throw error;
}
};
// ポケモンの詳細データ取得
export const getPokemon = async (url) => {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
throw error;
}
};
利点
- React Queryを使用した場合はキャッシュが機能し、1度表示したデータでは表示が速い
- ページングの処理が簡単
- ローディングやエラーの記載方法がシンプル
補足
React Query Dev Tools
- 主な機能
- クエリの確認
- データの確認
- インストール
npm install @tanstack/react-query-devtools
- 利用方法
-
ReactQueryDevtools
をインポートする - クエリプロバイダの子コンポーネントとして配置する
-
- コンポーネントの配置例
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools />
</QueryClientProvider>
</React.StrictMode>
);
- ツールが組み込まれるとブラウザ上に以下のアイコンが表示される
- ツールでのクエリ実行結果表示サンプル
isFetchingとisLoadingの違い
- isFetching
- 非同期処理のクエリが未完了の場合にtrue
- isLoading
- 非同期処理のクエリが未完了か、キャッシュデータがない場合にtrue
ステートとは
- ステートの例
- サーバーから取得したデータ
- UIの設定値(ライトモード・ダークモードなど)
- エラーリスト
- 等
- ステートに該当しない
- Props
- 固定値
- 他のステートやPropsから算出されるもの
ステートの種類
- グローバルステート
- ローカルステート
- サーバーステート
- クライアントステート