Pagination trong ReactJs

Khi làm các ứng dụng web, chúng ta luôn có nhu cầu lấy dữ liệu từ các remote server, API… các dữ liệu mạng xã hội, tin tức, shopping, thanh toán thì rất nhiều records, do đó chúng ta cần giải pháp phân trang để giảm tải sự hiển thị quá nhiều data gây khó chịu cho người dùng.

Hướng dẫn đơn giản sau đây giúp bạn dễ dàng phân trang trong app ReactJs

Ví dụ ở đây sử dụng API dữ liệu các quốc gia, 248 quốc gia tất cả. Chúng ta sẽ bắt đầu với ứng dụng react đơn giản.

Tạo một app react js

npx create-react-js pagination

Cài đặt các thư viện cần thiết

npm i bootstrap countries-api prop-types react-flags

Sau khi cài đặt thành công chúng ta copy folder flags ở địa chỉ node_modules\react-flags\vendor đưa vào folder public\img

Open file index.js, thêm code bootstrap

import "bootstrap/dist/css/bootstrap.min.css";

Components

Trong folder src, tạo folder components

Tạo 2 component

  • CountryCard.jsx Component
  • Pagination.jsx Component

CountryCard Component

import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import Flag from 'react-flags';

class CountryCard extends Component {
    render() {
        const { cca2: code2 = "", region = null, name = {} } = this.props.country || {};
        return (
            <Fragment>
                <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>
            </Fragment>
        )
    }
}

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

Pagination Component

 
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";

const LEFT_PAGE = "LEFT";
const RIGHT_PAGE = "RIGHT";

const range = (from, to, step = 1) => {
  let i = from;
  const range = [];

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

  return range;
};

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;

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

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

  fetchPageNumbers = () => {
    const totalPages = this.totalPages;
    const currentPage = this.state.currentPage;
    const pageNeighbours = this.pageNeighbours;

    const totalNumbers = this.pageNeighbours * 2 + 3;
    const totalBlocks = totalNumbers + 2;

    if (totalPages > totalBlocks) {
      let pages = [];

      const leftBound = currentPage - pageNeighbours;
      const rightBound = currentPage + pageNeighbours;
      const beforeLastPage = totalPages - 1;

      const startPage = leftBound > 2 ? leftBound : 2;
      const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage;

      pages = range(startPage, endPage);

      const pagesCount = pages.length;
      const singleSpillOffset = totalNumbers - pagesCount - 1;

      const leftSpill = startPage > 2;
      const rightSpill = endPage < beforeLastPage;

      const leftSpillPage = LEFT_PAGE;
      const rightSpillPage = RIGHT_PAGE;

      if (leftSpill && !rightSpill) {
        const extraPages = range(startPage - singleSpillOffset, startPage - 1);
        pages = &#91;leftSpillPage, ...extraPages, ...pages&#93;;
      } else if (!leftSpill && rightSpill) {
        const extraPages = range(endPage + 1, endPage + singleSpillOffset);
        pages = &#91;...pages, ...extraPages, rightSpillPage&#93;;
      } else if (leftSpill && rightSpill) {
        pages = &#91;leftSpillPage, ...pages, rightSpillPage&#93;;
      }

      return &#91;1, ...pages, totalPages&#93;;
    }

    return range(1, totalPages);
  };

  render() {
    if (!this.totalRecords) return null;

    if (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={e => this.handleClick(page, e)}
                  >
                    {page}
                  </a>
                </li>
              );
            })}
          </ul>
        </nav>
      </Fragment>
    );
  }
}

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

export default Pagination;

App.js

import React, { Component } from 'react';
import Countries from 'countries-api/lib/data/Countries.json';
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 allCountries = Countries;
    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 });
  };


  mapCountry(country, index) {
    return <CountryCard key={country.cca3} country={country} />
  }
  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();

    const displayCountries = currentCountries.map(this.mapCountry);
    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>
          {displayCountries}
        </div>
      </div>
    );
  }
}

export default App;


Download mã nguồn ở đây

GitHub