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.

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 dans la rubrique Dev