React

【React】ページネーションを自作する方法

Reactで外部API(JSON)のデータを「前へ」「次へ」で切り替えるページネーションを自作する方法です。next・previousのURLをuseStateで持ち、ボタンで取得し直す流れを、ポケモンAPIを例に説明します。

この記事の目次
  1. Reactでページネーションを自作する方法
  2. ファイル構成
  3. index.js
  4. App.js
  5. うまく動かないとき

Reactで外部API(JSON)から取得したデータで、次のデータと前のデータを表示するために、ページネーションを自作する方法を紹介します。
今回は、ポケモンAPIで実際に取得して表示します。

Reactのページネーションは、ライブラリを使わなくても「現在のページのデータ」「次ページのURL」「前ページのURL」の3つをuseStateで持ち、ボタンのクリックで次(前)のURLをfetchしてstateを入れ替えるだけで作れます。ここで扱うのは、APIのレスポンスにnextpreviousのURLが含まれるタイプです。ページ番号を自分で管理するタイプ(?page=2offsetで指定)でも、stateに持つものがURLからページ番号に変わるだけで考え方は同じです。

Reactでページネーションを自作する方法

ファイル構成

src
∟App.js
∟index.js
∟components
∟Card
∟Card.js
∟utils
get.js
App.jsでループして外部API(JSON)を取得した分だけCardを作成する構成とします。
utils/get.jsには、URLを受け取ってJSONを返す関数を置きます。以降のコードで使うgetAllSomeはこの関数です。

// utils/get.js
export const getAllSome = async (url) => {
  const res = await fetch(url);
  return res.json();
};

loadSomeは、一覧のresultsからCardに必要な詳細データを取得してsetSomeDataに渡す関数です。APIによっては一覧に必要な情報がすべて入っているので、その場合はsetSomeData(res.results)だけで済みます。

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>
  );

上でAppを読み込みます。
index.jsは特にいじるところはありません。

App.js

import { useEffect, useState } from "react";
  import Card from "./components/Card/Card";

  function App() {
    // 外部API
    const initialURL = "https://endpoint"; //endpoint
    const [loading, setLoading] = useState(true); //最初にloading出したいのでtrue
    const [someData, setSomeData] = useState([]);
    const [nextURL, setNextURL] = useState(""); //次の20件のURLの状態
    const [prevURL, setPrevURL] = useState(""); //前の20件のURLの状態

    useEffect(() => {
      const fetchSomeData = async () => {
        // すべてのポケモンデータを取得
        let res = await getAllSome(initialURL);
        // console.log(res);
        // console.log(res.next); //次の20件がある
        // console.log(res.previous); //前の20件がある
        // 詳細データを取得
        loadSome(res.results);
        setNextURL(res.next);
        setPrevURL(res.previous);
        setLoading(false);
      };
      fetchSomeData();
    }, []);


    const handlePrevPage = async () => {
      if(!prevURL) return; //1ページ目は前のデータはないので、returnしておく

      setLoading(true);

      let data = await getAllSome(prevURL);
      await loadSome(data.results);
      setNextURL(data.next);
      setPrevURL(data.previous);
      setLoading(false);
      // console.log(data);
    };
    const handleNextPage = async () => {
      if(!nextURL) return;

      setLoading(true);

      let data = await getAllSome(nextURL);
      await loadSome(data.results);
      setNextURL(data.next); //3ページ目以降のデータを取得するために状態を変更
      setPrevURL(data.previous); //2ページ目以降で前へのデータを取得する
      setLoading(false);
      // console.log(data);
    };
    return (
      <>
        <div className="App">
          {loading ? (
            <p className="loader">ロード中。。。</p>
          ) : (
            <>
              <div>
                {someData.map((some, i) => {
                  return <Card key={i} some={some} />;
                })}
              </div>
              <div className="btn">
                <button onClick={handlePrevPage}>前へ</button>
                <button onClick={handleNextPage}>次へ</button>
              </div>
            </>
          )}
        </div>
      </>
    );
  }

  export default App;

外部APIから取得して、Cardコンポーネントをmapを使ってデータを表示しています。keyの付け方やmapで表示されないときの原因はReactで配列を表示する方法|map・filter・keyと表示されない原因にまとめています。読み込み中の表示をスピナーにしたい場合は【React】ローディング(スピナー)を表示する方法を参考にしてください。
ページネーション部分のみを説明します。

<div className="btn">
  <button onClick={handlePrevPage}>前へ</button>
  <button onClick={handleNextPage}>次へ</button>
</div>

JSX部分で、ボタンを用意します。
そのボタンにはそれぞれ前のデータを取得するhandlePrevPage関数と次のデータを取得するhandleNextPage関数を設定しています。

const [nextURL, setNextURL] = useState(""); //次の20件のURLの状態
  const [prevURL, setPrevURL] = useState(""); //前の20件のURLの状態

前のページと次のページは現在のページで変わるため、状態管理をします。

const handlePrevPage = async () => {
  if(!prevURL) return; //1ページ目は前のデータはないので、returnしておく

  setLoading(true);

  let data = await getAllSome(prevURL);
  await loadSome(data.results);
  setNextURL(data.next);
  setPrevURL(data.previous);
  setLoading(false);
  // console.log(data);
};

まず、jsonに次のデータと前のデータが格納されているプロパティがあるはずなので、それを探します。上の場合は、data.nextとdata.previousというプロパティがある場合です。
handlePrevPage関数とhandleNextPage関数はほぼ同じなので、handlePrevPage関数だけ説明すると、取得したデータをロードしたら、それぞれの更新用関数に取得した次と前のデータをセットします。
そうすることで、常に現在のページの次と前のデータが保持されます。
あとは、前へボタン、次へボタンが押されたタイミングで、handlePrevPage関数とhandleNextPage関数を発火させれば完成です。初回のデータはuseEffectで取得しています。

うまく動かないとき

「前へ」「次へ」を押しても何も起きない場合は、data.nextdata.previousのプロパティ名がAPIのレスポンスと一致しているか、ブラウザの開発者ツールのNetworkタブで実際のJSONを確認します。最終ページでnextnullになるAPIなら、if(!nextURL) return;のガードで止まるので、ボタン自体をdisabled={!nextURL}にすると押せないことが見た目で伝わります。

また、React 18の開発モードではStrictModeによってuseEffectが2回実行され、初回のfetchが2回飛びます。本番ビルドでは1回になるので、開発中に2回リクエストが見えても不具合ではありません。

スポンサーリンク