Articolo originale: https://www.freecodecamp.org/news/build-a-custom-pagination-component-in-react/

Spesso ci troviamo a lavorare con applicazioni web che devono recuperare grandi quantità di dati da un server tramite API e presentarli sullo schermo.

Ad esempio, in un'applicazione per un social media si recuperano e si presentano i post e i commenti degli utenti. In una dashboard per la gestione delle risorse umane si visualizzano informazioni sui candidati che fanno domanda per un posto di lavoro. In un Client Email si mostrano le mail dell'utente.

Presentare questi dati tutti in una volta sullo schermo potrebbe causare un considerevole rallentamento nel caricamento della pagina web, a causa del gran numero di elementi DOM presenti nella pagina stessa.

Se vuoi ottimizzare le prestazioni, è possibile adottare varie tecniche per presentare i dati in modo più efficiente. Alcuni di questi metodi includono lo scorrimento infinito con virtualizzazione e la paginazione.

La paginazione funziona bene quando la dimensione dei dati è nota in anticipo e non si devono fare frequenti aggiunte o cancellazioni all'insieme di dati.

Ad esempio, in un sito web di social media dove i nuovi post sono pubblicati ogni pochi millisecondi, la paginazione non sarebbe la soluzione ideale. Tuttavia andrebbe bene per una dashboard per la gestione delle risorse umane, dove sono visualizzate le domande dei candidati e devono essere anche ordinate o filtrate.

In questo articolo, ci focalizzeremo sulla paginazione e costruiremo un componente controllato personalizzato che gestisce i pulsanti della pagina in base alla pagina corrente e al conteggio totale dei dati.

Scriveremo anche un hook React personalizzato che fornisce un intervallo di numeri renderizzato dal componente di paginazione. Potremo usare questo hook indipendentemente, così come quando vogliamo presentare un componente di paginazione con stili diversi o in una diversa progettazione.

Di seguito, una dimostrazione di quello che realizzeremo in questo tutorial:

PaginationDemo

Come Impostare il Progetto

Se hai familiarità con l'impostazione di un progetto React puoi saltare questa sezione.

Per impostare il progetto React useremo il pacchetto da riga di comando create-react-app. Puoi installare il pacchetto globalmente usando npm install -g create-react-app oppure yarn add global create-react-app.

Esegui create-react-app dalla riga di comando per creare un nuovo progetto, in questo modo:

npx create-react-app react-pagination

Successivamente, devi installare le dipendenze. Useremo una semplice dipendenza addizionale, chiamata classnames, che fornisce flessibilità nella gestione condizionale di valori multipli per l'attributo className.

Per installare il pacchetto, esegui npm install classnames oppure yarn add classnames.

Ora è possibile eseguire il progetto con il comando:

yarn start

oppure con il comando:

npm start

Come Definire l'Interfaccia

Ora che il progetto è in esecuzione, passeremo direttamente al componente di paginazione Pagination.

Prima di tutto, vediamo quali valori servono come props per il componente Pagination:

  • totalCount: rappresenta il conteggio totale dei dati disponibili dalla sorgente.
  • currentPage: rappresenta la pagina attiva corrente. Useremo un indice a base 1 invece del tradizionale indice a base 0 per il valore di currentPage.
  • pageSize: rappresenta il massimo dei dati visibili in una singola pagina.
  • onPageChange: funzione callback chiamata con il valore della pagina aggiornato quando la pagina viene cambiata.
  • siblingCount (opzionale): rappresenta il numero minimo di bottoni di pagina da mostrare su ciascun lato del bottone della pagina corrente. Valore predefinito: 1.
image-85
Illustrazione di diversi valori di siblingCount
  • className (opzionale): nome di classe da aggiungere al contenitore di livello superiore.

Dal componente di paginazione chiameremo l'hook usePagination che riceverà i seguenti parametri per calcolare gli intervalli di pagina: totalCount, currentPage, pageSize, siblingCount.

Come Implementare l'hook usePagination

Qui sotto ci sono alcune cose che occorre tenere a mente durante l'implementazione dell'hook usePagination:

  • L'hook di paginazione deve restituire l'intervallo di numeri da visualizzare nel componente di paginazione come un array.
  • La logica di calcolo deve essere rieseguita ogniqualvolta i valori di currentPage, pageSize, siblingCount ototalCount cambiano.
  • Il numero totale di elementi restituiti dall'hook dovrebbe rimanere costante. Questo eviterà il ridimensionamento del componente di paginazione se la lunghezza dell'array dell'intervallo cambia mentre l'utente sta interagendo con il componente.

Tenendo conto di quanto sopra, creiamo un file chiamato usePagination.js nella cartella src del progetto e iniziamo con l'implementazione.

Il codice di base è il seguente:

export const usePagination = ({
  totalCount,
  pageSize,
  siblingCount = 1,
  currentPage
}) => {
  const paginationRange = useMemo(() => {
     // La logica di implementazione andrà qui 
      
  }, [totalCount, pageSize, siblingCount, currentPage]);

  return paginationRange;
};

Se guardiamo il codice qui sopra, possiamo notare l'utilizzo dell'hook useMemo per calcolare la logica fondamentale. Il callback useMemo verrà eseguito quando cambia un qualunque valore nell'array di dipendenze.

Inoltre, impostiamo 1 come valore predefinito per la prop siblingCount come prop opzionale.

Prima di proseguire con l'implementazione della logica del codice, cerchiamo di comprendere i diversi comportamenti del componente Pagination. L'immagine qui sotto contiene gli stati possibili del componente di paginazione:

image-80
I diversi stati del componente Pagination

Nota che ci sono quattro stati possibili per il componente, che verranno analizzati uno per uno.

  • Il conteggio di pagine totale è minore delle icone di pagina che si vogliono mostrare. In questo caso si ritorna l'intervallo da 1 a totalPageCount.
  • Il conteggio di pagine totale è maggiore delle icone di pagina ma sono visibili solo i puntini a destra.
  • Il conteggio di pagine totale è maggiore delle icone di pagina ma sono visibili solo i puntini a sinistra.
  • Il conteggio di pagine totale è maggiore delle icone di pagina e sono visibili i puntini su entrambi i lati.

Come primo passo dobbiamo calcolare il numero totale di pagine da totalCount e pageSize, in questo modo:

const totalPageCount = Math.ceil(totalCount / pageSize);

Nota che stiamo usando Math.ceil per arrotondare il numero alla cifra più vicina al prossimo valore intero più alto. Questo assicura che venga riservata una pagina extra per  i dati rimanenti.

Successivamente, proseguiamo implementando una funzione personalizzata range, che riceve un valore  start e un valore end e restituisce un array con gli elementi da start (inizio) fino a end (fine):

const range = (start, end) => {
  let length = end - start + 1;
  /*
  	Crea un array di una certa lunghezza e imposta gli elementi all'interno dal valore di start al valore di end.
  */
  return Array.from({ length }, (_, idx) => idx + start);
};

Infine, implementeremo la logica fondamentale tenendo a mente i casi sopra citati.

n.d.t: per la comprensione del codice che segue, per DOTS si indende una costante definita ed esportata dall'hook usePagination, ed è una stringa che rappresenta 3 puntini.

export const usePagination = ({
  totalCount,
  pageSize,
  siblingCount = 1,
  currentPage
}) => {
  const paginationRange = useMemo(() => {
    const totalPageCount = Math.ceil(totalCount / pageSize);

    // Il conteggio delle pagine è determinato come siblingCount + firstPage + lastPage + currentPage + 2*DOTS
    const totalPageNumbers = siblingCount + 5;

    /*
      Caso 1:
      Se il numero di pagine è minore del numero da mostrare nel componente, restituiamo l'intervallo [1..totalPageCount]
    */
    if (totalPageNumbers >= totalPageCount) {
      return range(1, totalPageCount);
    }
	
    /*
    	Calcola gli indici di destra e sinistra e si assicura che siano all'interno dell'intervallo 1 e totalPageCount
    */
    const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
    const rightSiblingIndex = Math.min(
      currentPage + siblingCount,
      totalPageCount
    );

    /*
      Non mostriamo i puntini quando c'è un solo numero di pagina da inserire
      tra le estremità degli indici e i limiti di pagina, vale a dire 1 e totalPageCount.
      Quindi stiamo usando  leftSiblingIndex > 2 e rightSiblingIndex < totalPageCount - 2
    */
    const shouldShowLeftDots = leftSiblingIndex > 2;
    const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;

    const firstPageIndex = 1;
    const lastPageIndex = totalPageCount;

    /*
    	Caso 2: Non ci sono puntini a sinistra da mostrare ma 
        quelli a destra devono essere mostrati
    */
    if (!shouldShowLeftDots && shouldShowRightDots) {
      let leftItemCount = 3 + 2 * siblingCount;
      let leftRange = range(1, leftItemCount);

      return [...leftRange, DOTS, totalPageCount];
    }

    /*
    	Caso 3: Non ci sono puntini a destra da mostrare ma 
        quelli a sinistra devono essere mostrati
    */
    if (shouldShowLeftDots && !shouldShowRightDots) {
      
      let rightItemCount = 3 + 2 * siblingCount;
      let rightRange = range(
        totalPageCount - rightItemCount + 1,
        totalPageCount
      );
      return [firstPageIndex, DOTS, ...rightRange];
    }
     
    /*
    	Caso 4: Sia i puntini a destra che quelli a sinistra 
        sono da mostrare
    */
    if (shouldShowLeftDots && shouldShowRightDots) {
      let middleRange = range(leftSiblingIndex, rightSiblingIndex);
      return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
    }
  }, [totalCount, pageSize, siblingCount, currentPage]);

  return paginationRange;
};
usePagination Custom hook implementation 

L'idea dell'implementazione è che identifichiamo l'intervallo di numeri da mostrare nel componente, per poi unirli ai separatori o puntini, quando viene restituito l'intervallo finale.

Per il primo scenario dove  totalPageCount è minore del numero totale di icone, calcolato in base agli altri parametri, restituiamo semplicemente un intervallo di numeri  1..totalPageCount .

Per gli altri scenari, occorre identificare se servono i puntini alla sinistra o alla destra di currentPage calcolando gli indici di destra e sinistra dopo avere incluso le icone sorelle a currentPage e quindi prendere le decisioni.

Una volta che sappiamo dove vogliamo mostrare i puntini, il resto dei calcoli è piuttosto semplice.

Come Implementare il Componente Pagination

Come detto in precedenza, useremo l'hook usePagination nel componente di paginazione e verrà usato nell'interazione sull'intervallo restituito per presentarlo.

Creiamo un file Pagination.js nella cartella src e implementiamo la logica del codice in questo modo:

import React from 'react';
import classnames from 'classnames';
import { usePagination, DOTS } from './usePagination';
import './pagination.scss';
const Pagination = props => {
  const {
    onPageChange,
    totalCount,
    siblingCount = 1,
    currentPage,
    pageSize,
    className
  } = props;

  const paginationRange = usePagination({
    currentPage,
    totalCount,
    siblingCount,
    pageSize
  });

  // Se l'intervallo di paginazione è minore di due il componente non viene presentato
  if (currentPage === 0 || paginationRange.length < 2) {
    return null;
  }

  const onNext = () => {
    onPageChange(currentPage + 1);
  };

  const onPrevious = () => {
    onPageChange(currentPage - 1);
  };

  let lastPage = paginationRange[paginationRange.length - 1];
  return (
    <ul
      className={classnames('pagination-container', { [className]: className })}
    >
       {/* Freccia di navigazione sinistra */}
      <li
        className={classnames('pagination-item', {
          disabled: currentPage === 1
        })}
        onClick={onPrevious}
      >
        <div className="arrow left" />
      </li>
      {paginationRange.map(pageNumber => {
         
        // Se  pageItem è DOTS si presenta il carattere unicode per i puntini
        if (pageNumber === DOTS) {
          return <li className="pagination-item dots">&#8230;</li>;
        }
		
        // Presenta le pillole di pagina
        return (
          <li
            className={classnames('pagination-item', {
              selected: pageNumber === currentPage
            })}
            onClick={() => onPageChange(pageNumber)}
          >
            {pageNumber}
          </li>
        );
      })}
      {/*  Freccia di navigazione destra */}
      <li
        className={classnames('pagination-item', {
          disabled: currentPage === lastPage
        })}
        onClick={onNext}
      >
        <div className="arrow right" />
      </li>
    </ul>
  );
};

export default Pagination;
Implementazione di Pagination

Non presentiamo il componente Pagination se ci sono meno di due pagine (quindi restituiamo null).

Il componente  Pagination viene presentato come una lista con le frecce destra e sinistra che gestiscono le azioni di spostamento in avanti o indietro compiute dall'utente. Tra le frecce stabiliamo una corrispondenza attraverso paginationRange e presentiamo i numeri di pagina come pagination-items. Se l'elemento della pagina è DOTS, viene presentato il carattere unicode corrispondente ai puntini di sospensione.

Come gestione speciale, aggiungiamo una classe disabled alle frecce sinistra/destra per renderle inattive se currentPage è rispettivamente la prima o l'ultima pagina. Disabilitiamo pointer-events e aggiorniamo gli stili delle icone di freccia tramite CSS se l'icona deve essere disabilitata.

Vengono anche aggiunti gestori di evento click alle pillole di pagina che chiameranno la funzione callback onPageChanged con il valore aggiornato di  currentPage.

Il file CSS conterrà i seguenti stili:

.pagination-container {
  display: flex;
  list-style-type: none;

  .pagination-item {
    padding: 0 12px;
    height: 32px;
    text-align: center;
    margin: auto 4px;
    color: rgba(0, 0, 0, 0.87);
    display: flex;
    box-sizing: border-box;
    align-items: center;
    letter-spacing: 0.01071em;
    border-radius: 16px;
    line-height: 1.43;
    font-size: 13px;
    min-width: 32px;

    &.dots:hover {
      background-color: transparent;
      cursor: default;
    }
    &:hover {
      background-color: rgba(0, 0, 0, 0.04);
      cursor: pointer;
    }

    &.selected {
      background-color: rgba(0, 0, 0, 0.08);
    }

    .arrow {
      &::before {
        position: relative;
        /* top: 3pt; Togliere il commento per abbassare le icone come richiesto nei commenti*/
        content: '';
        /* Usando una scala em, le dimensione delle frecce sarà relativa a quella del font*/
        display: inline-block;
        width: 0.4em;
        height: 0.4em;
        border-right: 0.12em solid rgba(0, 0, 0, 0.87);
        border-top: 0.12em solid rgba(0, 0, 0, 0.87);
      }

      &.left {
        transform: rotate(-135deg) translate(-50%);
      }

      &.right {
        transform: rotate(45deg);
      }
    }

    &.disabled {
      pointer-events: none;

      .arrow::before {
        border-right: 0.12em solid rgba(0, 0, 0, 0.43);
        border-top: 0.12em solid rgba(0, 0, 0, 0.43);
      }

      &:hover {
        background-color: transparent;
        cursor: default;
      }
    }
  }
}
Stili del componente di paginazione

Questo è tutto!

L'implementazione di paginazione generica è pronta e possiamo usarla in qualunque parte del nostro codebase.

Come Usare il Componente di Paginazione Personalizzato

Come ultimo passo, incorporiamo il componente in un piccolo esempio.

Per lo scopo di questo articolo, presenteremo dati statici in forma di tabella. Quindi per prima cosa scriviamo:

import React from 'react';
import data from './data/mock-data.json';

export default function App() {
  return (
    <>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>FIRST NAME</th>
            <th>LAST NAME</th>
            <th>EMAIL</th>
            <th>PHONE</th>
          </tr>
        </thead>
        <tbody>
          {data.map(item => {
            return (
              <tr>
                <td>{item.id}</td>
                <td>{item.first_name}</td>
                <td>{item.last_name}</td>
                <td>{item.email}</td>
                <td>{item.phone}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </>
  );
}

A questo punto l'interfaccia utente si presenta come segue:

InfiniteTable-1

Ora occorrono due cose per incorporare il componente  Pagination.

  • Primo, manteniamo uno stato currentPage.
  • Secondo, calcoliamo i dati da presentare per una data pagina, ci iteriamo su per poi presentarli.

Per lo scopo di questa dimostrazione, manteniamo PageSize costante impostandone il valore a 10. Possiamo anche fornire un selettore per l'utente in modo che possa selezionare la dimensione della pagina desiderata.

Una volta effettuate le modifiche, possiamo proseguire con la presentazione del componente Pagination con le props appropriate.

Con queste modifiche in mente, il codice finale sarà questo:

import React, { useState, useMemo } from 'react';
import Pagination from '../Pagination';
import data from './data/mock-data.json';
import './style.scss';

let PageSize = 10;

export default function App() {
  const [currentPage, setCurrentPage] = useState(1);

  const currentTableData = useMemo(() => {
    const firstPageIndex = (currentPage - 1) * PageSize;
    const lastPageIndex = firstPageIndex + PageSize;
    return data.slice(firstPageIndex, lastPageIndex);
  }, [currentPage]);

  return (
    <>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>FIRST NAME</th>
            <th>LAST NAME</th>
            <th>EMAIL</th>
            <th>PHONE</th>
          </tr>
        </thead>
        <tbody>
          {currentTableData.map(item => {
            return (
              <tr>
                <td>{item.id}</td>
                <td>{item.first_name}</td>
                <td>{item.last_name}</td>
                <td>{item.email}</td>
                <td>{item.phone}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
      <Pagination
        className="pagination-bar"
        currentPage={currentPage}
        totalCount={data.length}
        pageSize={PageSize}
        onPageChange={page => setCurrentPage(page)}
      />
    </>
  );
}

Codice finale della dimostrazione

Ecco la dimostrazione dal vivo di questo tutorial:

Conclusione

In questo articolo abbiamo creato un hook personalizzato usePagination e lo abbiamo usato all'interno del componente Pagination. Abbiamo anche implementato una breve dimostrazione che fa uso di questo componente.

Puoi vedere l'intero codice sorgente per questo tutorial in questo repository GitHub.

Grazie per aver letto questo articolo.