開発者ドキュメント

Reactでカスタムページネーションを構築する方法

序章

多くの場合、リモートサーバー、API、またはデータベースから大量のデータレコードをフェッチする必要があるWebアプリケーションの構築に関与します。 たとえば、支払いシステムを構築している場合、何千ものトランザクションをフェッチしている可能性があります。 ソーシャルメディアアプリの場合、多くのユーザーコメント、プロファイル、またはアクティビティを取得している可能性があります。 いずれの場合でも、アプリを操作するエンドユーザーを圧倒しない方法でデータを表示するためのソリューションがいくつかあります。

大規模なデータセットを処理する1つの方法は、paginationを使用することです。 データセットのサイズ(データセット内のレコードの総数)が事前にわかっている場合、ページネーションは効果的に機能します。 次に、エンドユーザーとページネーションコントロールの相互作用に基づいて、データセット全体から必要なデータのチャンクのみをロードします。 これは、Google検索で検索結果を表示するために使用される手法です。

このチュートリアルでは、大規模なデータセットをページ付けするためにReactを使用してカスタムページ付けコンポーネントを構築する方法を学習します。 世界の国々のページ化されたビュー(既知のサイズのデータセット)を作成します。

これは、このチュートリアルで構築するもののデモです。

前提条件

このチュートリアルを完了するには、次のものが必要です。

このチュートリアルは、Nodev14.2.0で検証されました。 npm v6.14.4、 react v16.13.1、および react-scripts v3.4.1。

ステップ1—プロジェクトの設定

を使用して新しいReactアプリケーションを開始します create-react-app 指図。 アプリケーションには好きな名前を付けることができますが、このチュートリアルでは名前を付けます react-pagination:

  1. npx create-react-app react-pagination

次に、アプリケーションに必要な依存関係をインストールします。 まず、ターミナルウィンドウを使用して、プロジェクトディレクトリに移動します。

  1. cd react-pagination

次のコマンドを実行して、必要な依存関係をインストールします。

  1. npm install bootstrap@4.1.0 prop-types@15.6.1 react-flags@0.1.13 countries-api@2.0.1 node-sass@4.14.1

これによりインストールされます bootstrap, prop-types, react-flags, countries-api、 と node-sass.

インストールしました bootstrap デフォルトのスタイルが必要になるため、アプリケーションの依存関係としてパッケージ化します。 Bootstrapのスタイルも使用します pagination 成分。

アプリケーションにBootstrapを含めるには、 src/index.js ファイル:

  1. nano src/index.js

そして、次の行を他の行の前に追加します import ステートメント:

src / index.js
import "bootstrap/dist/css/bootstrap.min.css";

これで、Bootstrapスタイリングがアプリケーション全体で利用できるようになります。

また、インストールしました react-flags アプリケーションの依存関係として。 アプリケーションからフラグアイコンにアクセスするには、アイコン画像をにコピーする必要があります。 public アプリケーションのディレクトリ。

作成する img あなたのディレクトリ public ディレクトリ:

  1. mkdir public/img

で画像ファイルをコピーします flagsimg:

  1. cp -R node_modules/react-flags/vendor/flags public/img

これにより、すべてのコピーが提供されます react-flag アプリケーションへの画像。

いくつかの依存関係を含めたので、次のコマンドを実行してアプリケーションを起動します。 npm から react-pagination プロジェクトディレクトリ:

  1. npm start

アプリケーションを開始したので、開発を開始できます。 ライブリロード機能を備えたブラウザタブが開いており、開発中にアプリケーションとの同期を維持していることに注意してください。

この時点で、アプリケーションビューは次のスクリーンショットのようになります。

これで、コンポーネントの作成を開始する準備が整いました。

ステップ2—作成 CountryCard 成分

このステップでは、 CountryCard 成分。 The CountryCard コンポーネントは、特定の国の名前、地域、および旗をレンダリングします。

まず、作成しましょう components のディレクトリ src ディレクトリ:

  1. mkdir src/components

次に、新しいを作成します CountryCard.js のファイル src/components ディレクトリ:

  1. nano src/components/CountryCard.js

そして、それに次のコードスニペットを追加します。

src / components / CountryCard.js
import React from 'react';
import PropTypes from 'prop-types';
import Flag from 'react-flags';

const CountryCard = props => {
  const {
    cca2: code2 = '', region = null, name = {}
  } = props.country || {};

  return (
    <div className="col-sm-6 col-md-4 country-card">
      <div className="country-card-container border-gray rounded border mx-2 my-3 d-flex flex-row align-items-center p-0 bg-light">
        <div className="h-100 position-relative border-gray border-right px-2 bg-white rounded-left">
          <Flag country={code2} format="png" pngSize={64} basePath="./img/flags" className="d-block h-100" />
        </div>
        <div className="px-3">
          <span className="country-name text-dark d-block font-weight-bold">{ name.common }</span>
          <span className="country-region text-secondary text-uppercase">{ region }</span>
        </div>
      </div>
    </div>
  )
}

CountryCard.propTypes = {
  country: PropTypes.shape({
    cca2: PropTypes.string.isRequired,
    region: PropTypes.string.isRequired,
    name: PropTypes.shape({
      common: PropTypes.string.isRequired
    }).isRequired
  }).isRequired
};

export default CountryCard;

The CountryCard コンポーネントには country レンダリングされる国に関するデータを含む小道具。 に見られるように propTypes のために CountryCard コンポーネント、 country propオブジェクトには、次のデータが含まれている必要があります。

国オブジェクトのサンプルは次のとおりです。

{
  cca2: "NG",
  region: "Africa",
  name: {
    common: "Nigeria"
  }
}

また、を使用して国旗をレンダリングする方法にも注目してください react-flags パッケージ。 react-flagsのドキュメントをチェックして、必要な小道具とパッケージの使用方法の詳細を確認できます。

これで、個人が完成しました CountryCard 成分。 最終的には、 CountryCard■アプリケーションにさまざまなフラグと国情報を表示するために複数回。

ステップ3—作成 Pagination 成分

このステップでは、 Pagination 成分。 The Pagination コンポーネントには、ページネーションコントロールでページを作成、レンダリング、および切り替えるためのロジックが含まれています。

新しいを作成します Pagination.js のファイル src/components ディレクトリ:

  1. nano src/components/Pagination.js

そして、それに次のコードスニペットを追加します。

src / components / Pagination.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';

class Pagination extends Component {
  constructor(props) {
    super(props);
    const { totalRecords = null, pageLimit = 30, pageNeighbours = 0 } = props;

    this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 30;
    this.totalRecords = typeof totalRecords === 'number' ? totalRecords : 0;

    // pageNeighbours can be: 0, 1 or 2
    this.pageNeighbours = typeof pageNeighbours === 'number'
      ? Math.max(0, Math.min(pageNeighbours, 2))
      : 0;

    this.totalPages = Math.ceil(this.totalRecords / this.pageLimit);

    this.state = { currentPage: 1 };
  }
}

Pagination.propTypes = {
  totalRecords: PropTypes.number.isRequired,
  pageLimit: PropTypes.number,
  pageNeighbours: PropTypes.number,
  onPageChanged: PropTypes.func
};

export default Pagination;

The Pagination コンポーネントは、で指定されているように4つの特別な小道具を取ることができます propTypes 物体。

次の画像は、さまざまな値の効果を示しています pageNeighbours 小道具:

の中に constructor() 関数では、次のように合計ページを計算します。

this.totalPages = Math.ceil(this.totalRecords / this.pageLimit);

使用していることに注意してください Math.ceil() ここで、総ページ数の整数値を確実に取得します。 これにより、特に超過レコードの数がページごとに表示されるレコードの数より少ない場合に、超過レコードが最後のページに確実にキャプチャされます。

最後に、状態を初期化しました currentPage プロパティをに設定 1. 現在アクティブなページを内部で追跡するには、この状態プロパティが必要です。

次に、ページ番号を生成するためのメソッドを作成します。

後に importsしかし前に Pagination クラス、次の定数を追加し、 range 関数:

src / components / Pagination.js
// ...

const LEFT_PAGE = 'LEFT';
const RIGHT_PAGE = 'RIGHT';

/**
 * Helper method for creating a range of numbers
 * range(1, 5) => [1, 2, 3, 4, 5]
 */
const range = (from, to, step = 1) => {
  let i = from;
  const range = [];

  while (i <= to) {
    range.push(i);
    i += step;
  }

  return range;
}

の中に Pagination クラス、後 constructor、以下を追加します fetchPageNumbers 方法:

src / components / Pagination.js
class Pagination extends Component {
  // ...

  /**
   * Let's say we have 10 pages and we set pageNeighbours to 2
   * Given that the current page is 6
   * The pagination control will look like the following:
   *
   * (1) < {4 5} [6] {7 8} > (10)
   *
   * (x) => terminal pages: first and last page(always visible)
   * [x] => represents current page
   * {...x} => represents page neighbours
   */
  fetchPageNumbers = () => {
    const totalPages = this.totalPages;
    const currentPage = this.state.currentPage;
    const pageNeighbours = this.pageNeighbours;

    /**
     * totalNumbers: the total page numbers to show on the control
     * totalBlocks: totalNumbers + 2 to cover for the left(<) and right(>) controls
     */
    const totalNumbers = (this.pageNeighbours * 2) + 3;
    const totalBlocks = totalNumbers + 2;

    if (totalPages > totalBlocks) {
      const startPage = Math.max(2, currentPage - pageNeighbours);
      const endPage = Math.min(totalPages - 1, currentPage + pageNeighbours);
      let pages = range(startPage, endPage);

      /**
       * hasLeftSpill: has hidden pages to the left
       * hasRightSpill: has hidden pages to the right
       * spillOffset: number of hidden pages either to the left or to the right
       */
      const hasLeftSpill = startPage > 2;
      const hasRightSpill = (totalPages - endPage) > 1;
      const spillOffset = totalNumbers - (pages.length + 1);

      switch (true) {
        // handle: (1) < {5 6} [7] {8 9} (10)
        case (hasLeftSpill && !hasRightSpill): {
          const extraPages = range(startPage - spillOffset, startPage - 1);
          pages = [LEFT_PAGE, ...extraPages, ...pages];
          break;
        }

        // handle: (1) {2 3} [4] {5 6} > (10)
        case (!hasLeftSpill && hasRightSpill): {
          const extraPages = range(endPage + 1, endPage + spillOffset);
          pages = [...pages, ...extraPages, RIGHT_PAGE];
          break;
        }

        // handle: (1) < {4 5} [6] {7 8} > (10)
        case (hasLeftSpill && hasRightSpill):
        default: {
          pages = [LEFT_PAGE, ...pages, RIGHT_PAGE];
          break;
        }
      }

      return [1, ...pages, totalPages];
    }

    return range(1, totalPages);
  }
}

ここでは、最初に2つの定数を定義します。 LEFT_PAGERIGHT_PAGE. これらの定数は、それぞれ左右に移動するためのページコントロールがあるポイントを示すために使用されます。

ヘルパーも定義しました range() 数値の範囲を生成するのに役立つ関数。

注:プロジェクトで Lodash のようなユーティリティライブラリを使用する場合は、 _.range() 代わりにLodashによって提供される関数。 次のコードスニペットは、 range() 定義したばかりの関数とLodashの関数:

range(1, 5); // returns [1, 2, 3, 4, 5]
_.range(1, 5); // returns [1, 2, 3, 4]

次に、 fetchPageNumbers() のメソッド Pagination クラス。 このメソッドは、ページネーションコントロールに表示されるページ番号を生成するためのコアロジックを処理します。 最初のページと最後のページを常に表示する必要があります。

まず、いくつかの変数を定義しました。 totalNumbers コントロールに表示される合計ページ数を表します。 totalBlocks 表示されるページ番号の合計に、左右のページインジケータ用の2つの追加ブロックを加えたものを表します。

もしも totalPages より大きいではありません totalBlocks、から数値の範囲を返します 1totalPages. それ以外の場合は、ページ番号の配列を次のように返します。 LEFT_PAGERIGHT_PAGE ページがそれぞれ左と右にこぼれるポイントで。

ただし、ページネーションコントロールにより、最初のページと最後のページが常に表示されるようになっていることに注意してください。 左右のページコントロールが内側に表示されます。

ここで、を追加します render() ページネーションコントロールをレンダリングできるようにするメソッド。

の中に Pagination クラス、後 constructorfetchPageNumbers メソッド、以下を追加します render 方法:

src / components / Pagination.js
class Pagination extends Component {
  // ...

  render() {
    if (!this.totalRecords || this.totalPages === 1) return null;

    const { currentPage } = this.state;
    const pages = this.fetchPageNumbers();

    return (
      <Fragment>
        <nav aria-label="Countries Pagination">
          <ul className="pagination">
            { pages.map((page, index) => {

              if (page === LEFT_PAGE) return (
                <li key={index} className="page-item">
                  <a className="page-link" href="#" aria-label="Previous" onClick={this.handleMoveLeft}>
                    <span aria-hidden="true">&laquo;</span>
                    <span className="sr-only">Previous</span>
                  </a>
                </li>
              );

              if (page === RIGHT_PAGE) return (
                <li key={index} className="page-item">
                  <a className="page-link" href="#" aria-label="Next" onClick={this.handleMoveRight}>
                    <span aria-hidden="true">&raquo;</span>
                    <span className="sr-only">Next</span>
                  </a>
                </li>
              );

              return (
                <li key={index} className={`page-item${ currentPage === page ? ' active' : ''}`}>
                  <a className="page-link" href="#" onClick={ this.handleClick(page) }>{ page }</a>
                </li>
              );

            }) }

          </ul>
        </nav>
      </Fragment>
    );
  }
}

ここでは、ページ番号を生成します array を呼び出すことによって fetchPageNumbers() 以前に作成したメソッド。 次に、を使用して各ページ番号をレンダリングします Array.prototype.map(). クリックを処理するために、レンダリングされた各ページ番号にクリックイベントハンドラーを登録していることに注意してください。

また、次の場合、ページネーションコントロールはレンダリングされないことに注意してください。 totalRecords 小道具が正しく渡されませんでした Pagination コンポーネントまたはのみが存在する場合 1 ページ。

最後に、イベントハンドラーメソッドを定義します。

の中に Pagination クラス、後 constructorfetchPageNumbers メソッドと render メソッドには、以下を追加します。

src / components / Pagination.js
class Pagination extends Component {
  // ...

  componentDidMount() {
    this.gotoPage(1);
  }

  gotoPage = page => {
    const { onPageChanged = f => f } = this.props;
    const currentPage = Math.max(0, Math.min(page, this.totalPages));
    const paginationData = {
      currentPage,
      totalPages: this.totalPages,
      pageLimit: this.pageLimit,
      totalRecords: this.totalRecords
    };

    this.setState({ currentPage }, () => onPageChanged(paginationData));
  }

  handleClick = page => evt => {
    evt.preventDefault();
    this.gotoPage(page);
  }

  handleMoveLeft = evt => {
    evt.preventDefault();
    this.gotoPage(this.state.currentPage - (this.pageNeighbours * 2) - 1);
  }

  handleMoveRight = evt => {
    evt.preventDefault();
    this.gotoPage(this.state.currentPage + (this.pageNeighbours * 2) + 1);
  }
}

あなたは gotoPage() 状態を変更し、 currentPage 指定されたページに移動します。 それは page 引数の最小値は 1 総ページ数の最大値。 それは最終的に onPageChanged() 新しいページネーション状態を示すデータを使用して、小道具として渡された関数。

コンポーネントがマウントされたら、を呼び出して最初のページに移動します this.gotoPage(1) に示すように componentDidMount() ライフサイクル方式。

使用方法に注意してください (this.pageNeighbours * 2)handleMoveLeft()handleMoveRight() 現在のページ番号に基づいて、ページ番号をそれぞれ左と右にスライドします。

これは、左から右への動きの相互作用のデモです。

これで完了です Pagination 成分。 ユーザーは、このコンポーネントのナビゲーションコントロールを操作して、さまざまなページのフラグを表示できます。

ステップ4—構築 App 成分

今、あなたは CountryCardPagination コンポーネント、あなたはあなたの中でそれらを使用することができます App 成分。

を変更します App.js のファイル src ディレクトリ:

  1. nano src/App.js

の内容を置き換えます App.js 次のコード行を使用します。

src / App.js
import React, { Component } from 'react';
import Countries from 'countries-api';
import './App.css';
import Pagination from './components/Pagination';
import CountryCard from './components/CountryCard';

class App extends Component {
  state = { allCountries: [], currentCountries: [], currentPage: null, totalPages: null }

  componentDidMount() {
    const { data: allCountries = [] } = Countries.findAll();
    this.setState({ allCountries });
  }

  onPageChanged = data => {
    const { allCountries } = this.state;
    const { currentPage, totalPages, pageLimit } = data;
    const offset = (currentPage - 1) * pageLimit;
    const currentCountries = allCountries.slice(offset, offset + pageLimit);

    this.setState({ currentPage, currentCountries, totalPages });
  }
}

export default App;

ここで初期化します App 次の属性を持つコンポーネントの状態:

次に、 componentDidMount() ライフサイクル方式では、を使用してすべての世界の国をフェッチします countries-api 呼び出しによるパッケージ Countries.findAll(). 次に、アプリの状態を更新し、設定します allCountries すべての世界の国々を含むために。 パッケージの詳細については、countries-apiドキュメントを参照してください。

最後に、 onPageChanged() メソッド。ページネーションコントロールから新しいページに移動するたびに呼び出されます。 このメソッドはに渡されます onPageChanged の小道具 Pagination 成分。

この方法で注意を払う価値のある2つの行があります。 最初はこの行です:

const offset = (currentPage - 1) * pageLimit;

The offset valueは、現在のページのレコードをフェッチするための開始インデックスを示します。 使用する (currentPage - 1) オフセットがゼロベースであることを確認します。 たとえば、表示しているとしましょう 25 ページごとのレコード、および現在ページを表示しています 5. そうして offset になります ((5 - 1) * 25 = 100).

たとえば、データベースからオンデマンドでレコードをフェッチしている場合、これはオフセットの使用方法を示すサンプルSQLクエリです。

SELECT * FROM `countries` LIMIT 100, 25

データベースや外部ソースからレコードをフェッチしていないため、現在のページに表示される必要なレコードのチャンクを抽出する方法が必要です。

2番目はこの行です:

const currentCountries = allCountries.slice(offset, offset + pageLimit);

ここでは、 Array.prototype.slice() 必要なレコードのチャンクを抽出するメソッド allCountries を渡すことによって offset スライスの開始インデックスとして (offset + pageLimit) スライスを終了する前のインデックスとして。

注:このチュートリアルでは、外部ソースからレコードをフェッチしていません。 実際のアプリケーションでは、おそらくデータベースまたはAPIからレコードをフェッチすることになります。 レコードをフェッチするためのロジックは、 onPageChanged() の方法 App 成分。

架空のAPIエンドポイントがあるとしましょう /api/countries?page={current_page}&limit={page_limit}. 次のスニペットは、 axiosHTTPパッケージを使用してAPIからオンデマンドで国を取得する方法を示しています。

onPageChanged = data => {
  const { currentPage, totalPages, pageLimit } = data;

  axios.get(`/api/countries?page=${currentPage}&limit=${pageLimit}`)
    .then(response => {
      const currentCountries = response.data.countries;
      this.setState({ currentPage, currentCountries, totalPages });
    });
}

今、あなたは仕上げることができます App コンポーネントを追加して render() 方法。

の中に App クラス、しかし後 componentDidMountonPageChanged、以下を追加します render 方法:

src / App.js
class App extends Component {
  // ... other methods here ...

  render() {
    const { allCountries, currentCountries, currentPage, totalPages } = this.state;
    const totalCountries = allCountries.length;

    if (totalCountries === 0) return null;

    const headerClass = ['text-dark py-2 pr-4 m-0', currentPage ? 'border-gray border-right' : ''].join(' ').trim();

    return (
      <div className="container mb-5">
        <div className="row d-flex flex-row py-5">
          <div className="w-100 px-4 py-5 d-flex flex-row flex-wrap align-items-center justify-content-between">
            <div className="d-flex flex-row align-items-center">
              <h2 className={headerClass}>
                <strong className="text-secondary">{totalCountries}</strong> Countries
              </h2>
              { currentPage && (
                <span className="current-page d-inline-block h-100 pl-4 text-secondary">
                  Page <span className="font-weight-bold">{ currentPage }</span> / <span className="font-weight-bold">{ totalPages }</span>
                </span>
              ) }
            </div>
            <div className="d-flex flex-row py-4 align-items-center">
              <Pagination totalRecords={totalCountries} pageLimit={18} pageNeighbours={1} onPageChanged={this.onPageChanged} />
            </div>
          </div>
          { currentCountries.map(country => <CountryCard key={country.cca3} country={country} />) }
        </div>
      </div>
    );
  }
}

の中に render() メソッドでは、国の総数、現在のページ、ページの総数、 <Pagination> コントロール、そして <CountryCard> 現在のページの国ごとに。

合格したことに注意してください onPageChanged() 以前に定義したメソッド onPageChanged の小道具 <Pagination> コントロール。 これは、ページの変更をキャプチャするために非常に重要です。 Pagination 成分。 あなたも表示しています 18 ページあたりの国。

この時点で、アプリは次のスクリーンショットのようになります。

あなたは今持っています App 複数を表示するコンポーネント CountryCard コンポーネントと Pagination コンテンツを個別のページに分割するコンポーネント。 次に、アプリケーションのスタイリングについて説明します。

ステップ5—カスタムスタイルを追加する

以前に作成したコンポーネントにいくつかのカスタムクラスを追加していることに気付いたかもしれません。 これらのクラスのスタイルルールをいくつか定義しましょう。 src/App.scss ファイル。

  1. nano src/App.scss

The App.scss ファイルは次のスニペットのようになります。

src / App.scss
/* Declare some variables */
$base-color: #ced4da;
$light-background: lighten(desaturate($base-color, 50%), 12.5%);

.current-page {
  font-size: 1.5rem;
  vertical-align: middle;
}

.country-card-container {
  height: 60px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

.country-name {
  font-size: 0.9rem;
}

.country-region {
  font-size: 0.7rem;
}

.current-page,
.country-name,
.country-region {
  line-height: 1;
}

// Override some Bootstrap pagination styles
ul.pagination {
  margin-top: 0;
  margin-bottom: 0;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);

  li.page-item.active {
    a.page-link {
      color: saturate(darken($base-color, 50%), 5%) !important;
      background-color: saturate(lighten($base-color, 7.5%), 2.5%) !important;
      border-color: $base-color !important;
    }
  }

  a.page-link {
    padding: 0.75rem 1rem;
    min-width: 3.5rem;
    text-align: center;
    box-shadow: none !important;
    border-color: $base-color !important;
    color: saturate(darken($base-color, 30%), 10%);
    font-weight: 900;
    font-size: 1rem;

    &:hover {
      background-color: $light-background;
    }
  }
}

あなたの App.js 参照するファイル App.scss それ以外の App.css.

注:これについて詳しくは、 CreateReactAppドキュメントを参照してください。

  1. nano src/App.js
src / App.js
import React, { Component } from 'react';
import Countries from 'countries-api';
import './App.scss';
import Pagination from './components/Pagination';
import CountryCard from './components/CountryCard';

スタイルを追加すると、アプリは次のスクリーンショットのようになります。

これで、追加のカスタムスタイルを備えた完全なアプリケーションができました。 カスタムスタイルを使用して、Bootstrapなどのライブラリによって提供されるデフォルトのスタイルを変更および拡張できます。

結論

このチュートリアルでは、Reactアプリケーションでカスタムページネーションウィジェットを作成しました。 このチュートリアルでは、APIを呼び出したり、データベースバックエンドとやり取りしたりしていませんが、アプリケーションでそのようなやり取りが必要になる場合があります。 このチュートリアルで使用されているアプローチに制限されることはありません。アプリケーションの要件に合わせて、アプローチを拡張できます。

このチュートリアルの完全なソースコードについては、GitHubのbuild-react-pagination-demoリポジトリを確認してください。 また、CodeSandboxでこのチュートリアルのライブデモを入手することもできます。

Reactの詳細については、 React.js シリーズのコーディング方法をご覧になるか、Reactトピックページで演習やプログラミングプロジェクトを確認してください。

モバイルバージョンを終了