4
1

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でPokeAPIの無限スクロールを実装してみた

Last updated at Posted at 2024-01-24

この記事で分かること

  • 無限スクロール+叩くAPIの切り替え(無限スクロール+検索機能のようなイメージ)
  • React QueryのuseInfiniteQueryの使い方
  • react-infinite-scrollerのコンポーネントの使い方
  • useInfiniteQueryとreact-infinite-scrollerを使った無限スクロールの実装方法

React Queryとは

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

動作概要

  • useInfiniteQueryのqueryFnで設定した関数がAPIを叩く
  • APIで取得した結果が画面に表示される
  • <InfiniteScroll></InfiniteScroll>で囲んだ領域が画面下部までスクロールされるとfetchNextPage()が呼ばれ、続きのデータを取得する
    • fetchNextPage()で叩くAPIはuseInfiniteQueryのgetNextPageParamで設定したもの
  • 続きのデータが追加で表示される
  • 画面下部までスクロールすると再度データを取得し、以下繰り返し

サンプルコード

全ソースコードはこちら

※CORS対応していないので、環境によってはCORSエラーが発生する可能性があります

機能概要

  • PokeAPIにアクセスし、初期状態ではポケモンリストを取得する
  • アビリティリストボタンをクリックするとアビリティリストに表示が切り替わる(叩くAPIが変わる)
  • ポケモンリストボタンをクリックするとポケモンリストに表示が切り替わる(叩くAPIが変わる)

実行結果

ポケモンリスト

pokelist.png

アビリティリスト

abilitylist.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 } from './utils/pokemon';
import Card from './components/Card';
import { useInfiniteQuery } from '@tanstack/react-query';
import InfiniteScroll from 'react-infinite-scroller';

function App() {
  const [url, setUrl] = useState('https://pokeapi.co/api/v2/pokemon');
  const [queryKey, setQueryKey] = useState('list');

  // ポケモン一覧を取得するuseInfiniteQuery
  const {
    data: pokemonList,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isLoading,
    isError,
    error,
  } = useInfiniteQuery({
    queryKey: ['pokemonList', queryKey],
    queryFn: ({ pageParam = url }) => getAllPokemon(pageParam),
    getNextPageParam: (lastPage) => {
      return lastPage.next || undefined;
    },
  });

  // ポケモン一覧を表示
  const pokeList = () => {
    setUrl('https://pokeapi.co/api/v2/pokemon');
    setQueryKey('list');
  };

  // アビリティ一覧を表示
  const pokeAbility = () => {
    setUrl('https://pokeapi.co/api/v2/ability');
    setQueryKey('ability');
  };

  if (isLoading) {
    return <div className="App">Loading...</div>;
  }

  if (isError) {
    return <div className="App">Error! {error.toString()}</div>;
  }

  // ポケモンデータの表示
  return (
    <div className="App">
      <>
        <div style={{ margin: '30px' }}>
          <button style={{ marginRight: '20px' }} onClick={() => pokeList()}>
            ポケモンリスト
          </button>
          <button onClick={() => pokeAbility()}>アビリティリスト</button>
        </div>

        {/* 各ポケモンの表示 */}
        <InfiniteScroll
          loadMore={() => {
            if (!isFetching) {
              fetchNextPage();
            }
          }}
          hasMore={hasNextPage}
        >
          {pokemonList.pages.map((pageData) => {
            return pageData.results.map((pokemon, i) => {
              return <Card key={i} pokemon={pokemon} />;
            });
          })}
        </InfiniteScroll>
        {isFetching && <div style={{ margin: '50px' }}>Loading...</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">
        {!pokemon.detail.sprites ? (
          <></>
        ) : (
          <img src={pokemon.detail.sprites.front_default} alt="" />
        )}
      </div>
      name: {pokemon.name}
      <br />
      {!pokemon.detail.weight ? <></> : <>weight: {pokemon.detail.weight}</>}
    </div>
  );
};

export default Card;

src/utils/pokemon.js

// ポケモン一覧の取得
export const getAllPokemon = async (url, offset = 0) => {
  try {
    // 一覧取得
    const response = await fetch(`${url}`);
    const data = await response.json();

    console.log(data);

    // 一覧から詳細データを取得
    let _pokemonData = await Promise.all(
      data.results.map((pokemon) => {
        let pokemonRecord = getPokemon(pokemon.url);
        return pokemonRecord;
      })
    );

    console.log(_pokemonData);

    // 一覧データに詳細データを組み合わせる
    const details = data.results.map((pokemon, i) => {
      return { ...pokemon, detail: _pokemonData[i] };
    });

    data.results = details;

    return data;
  } catch (error) {
    throw error;
  }
};

// ポケモンの詳細データ取得
const getPokemon = async (url) => {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return data;
  } catch (error) {
    throw error;
  }
};

つまづきポイント

  • 叩くAPIを変更した場合、queryKeyを変更しないと表示に反映されない(キャッシュが使用されるため)
  • 対象となるAPIに次のデータを示すデータが含まれていないとgetNextPageParamに指定するキーが無い(APIを自作する場合は要注意)

参考

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?