0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React QueryでPokeAPIを叩いてみた

Last updated at Posted at 2024-01-14

この記事で分かること

  • React Queryでのデータ取得方法
  • React Queryを使用した場合と使用しない場合の違い

React Queryとは

  • TanStack QueryのReactアダプター
    • TanStack QueryとReactフレームワークの間でデータの取り扱いや通信を調整する
  • データの取得、キャッシュ、更新の仕組みを提供する
    • サーバーステートの管理ライブラリであると説明されている

React Queryが提供する機能

  • ローディング
  • エラー
  • ページング
  • 無限スクロール
  • プリフェッチ
  • データの更新
  • 重複排除(De-duplication)
  • リトライ
  • コールバック

利用手順

  • パッケージのインストール
    • package.jsonの有るディレクトリで以下を実行
      • npm install @tanstack/react-query
  • 親コンポーネント(今回はindex.js)でクエリクライアントの作成
    • クエリクライアントはクエリとキャッシュを管理する
  • 親コンポーネント(今回はindex.js)でクエリプロバイダを設定
    • Propsとしてクエリクライアント渡す
  • 子コンポーネント(今回はApp.js)でuseQueryを呼び出す
  • userQueryの引数としてqueryKeyとqueryFnが必須
  • queryKeyはクエリの実行結果を一意に特定し、キャッシュに利用される(queryKeyが同じならAPIを叩かずにuseQueryのキャッシュデータが利用される)
  • queryFnは実際にAPIを叩く関数を指定する

サンプルコード

全ソースコードはこちら

機能概要

  • PokeAPIにアクセスし、ポケモン情報を取得する
  • 1ページあたり20体のポケモンを表示する
  • Prevボタンで前の20体を表示する
  • Nextボタンで次の20体を表示する

実行結果

result.png

フォルダ構成(抜粋)

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>
);
  • ツールが組み込まれるとブラウザ上に以下のアイコンが表示される

react_query_dev_tool_icon.png

  • ツールでのクエリ実行結果表示サンプル

react_query_dev_tool_result.png

isFetchingとisLoadingの違い

  • isFetching
    • 非同期処理のクエリが未完了の場合にtrue
  • isLoading
    • 非同期処理のクエリが未完了か、キャッシュデータがない場合にtrue

ステートとは

  • ステートの例
    • サーバーから取得したデータ
    • UIの設定値(ライトモード・ダークモードなど)
    • エラーリスト
  • ステートに該当しない
    • Props
    • 固定値
    • 他のステートやPropsから算出されるもの

ステートの種類

  • グローバルステート
  • ローカルステート
  • サーバーステート
  • クライアントステート

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?