Comment paginer avec Meteor.js

Maîtrisez la pagination MongoDB avec Meteor.js : gérez le wrapper custom, le protocole DDP et organisez vos collections pour un frontend performant.
Dernièrement, je vous parlais un peu de Meteor.js, au travers de deux articles que je vous invite à lire si ce n'est pas le cas :
C'est une technologie que j'apprécie beaucoup et qui me permet d'accélérer la création de projets avec un côté temps réel. Pour rappel, un des principaux avantages de Meteor, c'est sa capacité à propager le changement d'une donnée en base à tous les clients connectés.
Gestion de la pagination dans un HoC withTracker
WithTracker est le HoC (higher order component) de Meteor.js qui vous permet de faire le lien avec votre couche de données. Il convient alors de faire toute la partie manipulation de données à cet endroit.
Nous n'entrerons pas dans les détails de la définition des types et la définitions des API (que nous avons vu dans les articles précédents).
Voici une proposition d'organisation pour paginer vos pages avec Meteor et MongoDB.
Organisation de la page
import * as React from 'react'
import { useFind, useSubscribe, withTracker } from 'meteor/react-meteor-data'
import Spinner from '@components/spinner'
import Paginator from '@components/paginator'
import { ProductCollection } from '/imports/api'
type PageProps = {
products: Products[]
count: number
limit: number
toPage: (p: number) => boolean
isLoading: () => boolean
}
function Page({
products,
toPage,
count,
limit,
isLoading
}: PageProps) {
return (
<div>
{/* ... page */}
{isLoading() && <Spinner />}
{/* ... display products */}
<Paginator
toPage={toPage}
count={count}
limit={limit}
displayPages={true}
/>
</div>
)
}
export default withTracker(() => {
const isLoading = useSubscribe('products')
const [currentPage, setCurrentPage] = React.useState(1)
const limit = 15 // Limite par défault = 15 éléments
const products = useFind(
() => ProductCollection.find({}, { limit, skip: limit * (currentPage - 1) }),
[currentPage, limit] // dépendances
) // On récupère les produits avec un offset
return {
limit,
count: ProductCollection.find({}, { fields: { _id: 1 } }).count() ?? 0,
products,
toPage: (nextPage: number) => {
const count = ProductCollection.find({}, { fields: { _id: 1 } }).count() ?? 0
if (nextPage < count) {
setCurrentPage(nextPage)
return true
}
return false
},
isLoading,
}
})(Page)
Vous remarquerez que nous avons mis un state dans le HoC, il va gérer la pagination basé sur un système d'offset/limit.
Composant de pagination
Ensuite, je pagine via ce composant :
import * as React from 'react'
import cn from 'classnames'
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'
type PaginatorProps = {
toPage: (p: number) => boolean
defaultPage?: number
hasNextPage?: boolean
hasPrevPage?: boolean
displayPages?: boolean
maxVisiblePages?: number
limit: number
count: number
}
type FooterPage = {
currentPage: number
label: string
onClick: (e: React.SyntheticEvent) => void
}
const Paginator = ({
defaultPage = 1,
toPage,
hasNextPage,
hasPrevPage,
displayPages,
maxVisiblePages = 5,
limit,
count,
}: PaginatorProps) => {
const [page, setPage] = React.useState(defaultPage)
const [visiblePages, setVisiblePages] = React.useState<FooterPage[]>([])
const updatePage = (newPage: number) => {
const pageChanged = toPage(newPage)
if (pageChanged) {
setPage(newPage)
let newVisiblePages: FooterPage[] = []
const start = newPage < 2 ? 0 : newPage - 2
const lastPage = Math.ceil(count / limit)
const end = start + maxVisiblePages < lastPage ? start + maxVisiblePages : lastPage
for (let i = start; i < end; i++) {
newVisiblePages.push({
label: (i + 1).toString(),
currentPage: i + 1,
onClick: () => (newPage - 1 === i ? null : updatePage(i + 1)),
})
}
setVisiblePages(newVisiblePages)
}
}
React.useEffect(() => {
const lastPage = Math.ceil(count / limit)
let newVisiblePages: FooterPage[] = []
const start = page < 2 ? 0 : page - 2
for (let i = start; i < lastPage; i++) {
newVisiblePages.push({
label: (i + 1).toString(),
currentPage: i + 1,
onClick: () => (page - 1 === i ? null : updatePage(i + 1)),
})
}
setVisiblePages(newVisiblePages)
}, [limit, count])
return (
<div className="pagination">
{hasPrevPage && (
<button
className="btn btn-light"
onClick={(e) => {
e.stopPropagation()
updatePage(page - 1)
}}
>
<FaAngleLeft size={28} />
</button>
)}
{displayPages && (
<div className="inline">
{maxVisiblePages &&
visiblePages.map((p, index) => {
return (
<button
className={cn(
'btn me-2',
p.currentPage === page ? 'btn-dark' : 'btn-light'
)}
onClick={p.onClick}
key={index}
>
{p.label}
</button>
)
})}
</div>
)}
{hasNextPage && (
<button
className="btn btn-light"
onClick={(e) => {
e.stopPropagation()
updatePage(page + 1)
}}
>
<FaAngleRight size={28} />
</button>
)}
</div>
)
}
export default Paginator
Vous devriez avoir un composant qui ressemble à cela :

En sachant que vous avez les props hasNextPage et hasPrevPage qui affichent les chevrons si vous les passez à true. (Ces propriétés sont optionnelles).
De même, vous pouvez ou non afficher les pages sous forme de numéro en retirant la propriété displayPages.
FAQ
Pourquoi mettre le state de pagination dans le HoC withTracker plutôt que dans le composant Page ?
WithTracker est le point d'entrée des données Meteor, c'est donc là que la logique de récupération doit vivre. Placer le state de la page courante dans le HoC permet de recalculer automatiquement la requête MongoDB avec le bon offset dès que l'utilisateur change de page.
Comment fonctionne concrètement le système d'offset pour récupérer les bons éléments ?
La requête utilise les options limit et skip de MongoDB. Pour la page 1, skip vaut 0 ; pour la page 2, skip vaut 15 (avec une limite de 15), et ainsi de suite. C'est la formule skip: limit * (currentPage - 1) qui gère ce calcul.
À quoi sert le count récupéré avec seulement le champ _id ?
Récupérer uniquement le champ _id permet de compter le nombre total de documents sans rapatrier toutes les données via DDP. C'est une optimisation importante pour éviter de surcharger le protocole temps réel de Meteor.
Les chevrons précédent/suivant et les numéros de page sont-ils obligatoires ?
Non, tout est optionnel. Les props hasNextPage et hasPrevPage contrôlent l'affichage des chevrons, et displayPages permet d'activer ou non les boutons numérotés. Vous pouvez combiner ces options selon vos besoins d'interface.
Peut-on changer le nombre d'éléments affichés par page ?
Oui, il suffit de modifier la valeur de la variable limit dans le HoC, qui est passée en prop au composant Paginator. Ce dernier s'en sert pour recalculer le nombre total de pages et mettre à jour la liste des pages visibles.

Alexandre P.
Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.
Poursuivre la lecture


