Pagination
Merci de mettre à jour vers la dernière version (≥ 0.3.0) pour utiliser cette API. L'ancienne API useSWRPages
est maintenant obsolète.
SWR fournie une API dédiée useSWRInfinite
pour supporter les patterns UI courants comme la pagination et le chargement infini.
Quand utiliser useSWR
Pagination
Avant tout, on pourrait NE PAS avoir besoin de useSWRInfinite
mais utiliser simplement useSWR
si on construit quelque chose comme ça :
...qui est une UI de pagination typique. Voyons comment on peut l'implémenter facilement avec useSWR
:
function App () {
const [pageIndex, setPageIndex] = useState(0);
// L'URL de l'API inclut l'index de la page, qui est un état React.
const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
// ... gérer les états de chargement et d'erreur
return <div>
{data.map(item => <div key={item.id}>{item.name}</div>)}
<button onClick={() => setPageIndex(pageIndex - 1)}>Précédent</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Suivant</button>
</div>
}
En outre, on peut créer une abstraction pour ce "composant de page" :
function Page ({ index }) {
const { data } = useSWR(`/api/data?page=${index}`, fetcher);
// ... gérer les états de chargement et d'erreur
return data.map(item => <div key={item.id}>{item.name}</div>)
}
function App () {
const [pageIndex, setPageIndex] = useState(0);
return <div>
<Page index={pageIndex}/>
<button onClick={() => setPageIndex(pageIndex - 1)}>Précédent</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Suivant</button>
</div>
}
A cause du cache de SWR, on bénéficie du préchargement de la page suivante. On rend la page suivante dans un div caché, donc SWR va déclencher le chargement de la page suivante. Quand l'utilisateur navigue vers la page suivante, les données sont déjà là :
function App () {
const [pageIndex, setPageIndex] = useState(0);
return <div>
<Page index={pageIndex}/>
<div style={{ display: 'none' }}><Page index={pageIndex + 1}/></div>
<button onClick={() => setPageIndex(pageIndex - 1)}>Précédent</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Suivant</button>
</div>
}
Avec seulement 1 ligne de code, on obtient une meilleure UX. Le hook useSWR
est si puissant que la plupart des scénarios sont couverts par lui.
Chargement infini
Parfois, on veut construire une UI de chargement infini, avec un bouton "Charger plus" qui ajoute des données à la liste (ou fait automatiquement quand on défile vers le bas) :
Pour implémenter ceci, on a besoin de faire un nombre dynamique de requêtes sur cette page. Les hooks React ont quelques règles (opens in a new tab), donc on NE PEUT PAS faire quelque chose comme ça :
function App () {
const [cnt, setCnt] = useState(1)
const list = []
for (let i = 0; i < cnt; i++) {
// 🚨 C'est faux ! En général, on ne peut pas utiliser des hooks dans une boucle.
const { data } = useSWR(`/api/data?page=${i}`)
list.push(data)
}
return <div>
{list.map((data, i) =>
<div key={i}>{
data.map(item => <div key={item.id}>{item.name}</div>)
}</div>)}
<button onClick={() => setCnt(cnt + 1)}>En récupérer plus</button>
</div>
}
Au lieu de ça, on peut utiliser l'abstraction <Page />
qu'on a créé pour y arriver :
function App () {
const [cnt, setCnt] = useState(1)
const pages = []
for (let i = 0; i < cnt; i++) {
pages.push(<Page index={i} key={i} />)
}
return <div>
{pages}
<button onClick={() => setCnt(cnt + 1)}>En récupérer plus</button>
</div>
}
Cas avancés
Cependant, dans certains cas avancés, la solution ci-dessus ne fonctionne pas.
Par exemple, on implémente toujours la même UI "En récupérer plus", mais on a aussi besoin d'afficher le nombre total d'éléments. On ne peut pas utiliser la solution <Page />
parce que l'UI de niveau supérieur (<App />
) a besoin des données de chaque page :
function App () {
const [cnt, setCnt] = useState(1)
const pages = []
for (let i = 0; i < cnt; i++) {
pages.push(<Page index={i} key={i} />)
}
return <div>
<p>??? éléments</p>
{pages}
<button onClick={() => setCnt(cnt + 1)}>En récupérer plus</button>
</div>
}
Aussi, si l'API de pagination est basée sur un curseur, cette solution ne fonctionne pas non plus. Parce que chaque page a besoin des données de la page précédente, elles ne sont pas isolées.
C'est là que le nouveau hook useSWRInfinite
peut aider.
useSWRInfinite
useSWRInfinite
vous donne la possibilité de déclencher un nombre de requêtes avec un seul hook. Voici à quoi ça ressemble :
import useSWRInfinite from 'swr/infinite'
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
Semblable à useSWR
, ce nouveau hook accepte une fonction qui retourne la clé de la requête, une fonction fetcher, et des options. Il retourne toutes les valeurs que useSWR
retourne, incluant 2 valeurs supplémentaires : la taille de la page et un setter de taille de page, comme un état React.
Dans le chargement infini, une page est une requête, et notre but est de récupérer plusieurs pages et de les afficher.
Si vous utilisez les versions SWR 0.x, useSWRInfinite
doit être importé depuis swr
:
import { useSWRInfinite } from 'swr'
API
Paramètres
getKey
: une fonction qui accepte l'index et les données de la page précédente, et retourne la clé d'une pagefetcher
: identique à la fonction fetcher deuseSWR
options
: accepte toutes les options queuseSWR
supporte, avec 4 options supplémentaires :initialSize = 1
: le nombre de pages qui doivent être chargées initialementrevalidateAll = false
: toujours essayer de revalider toutes les pagesrevalidateFirstPage = true
: toujours essayer de revalider la première pagepersistSize = false
: ne pas réinitialiser la taille de la page à 1 (ouinitialSize
si défini) quand la clé de la première page changeparallel = false
: récupère plusieurs pages en parallèle
Notez que l'option initialSize
n'est pas autorisée à changer dans le cycle de vie.
Valeurs retournées
data
: un tableau des valeurs de réponse de chaque pageerror
: identique àerror
deuseSWR
isLoading
: identique àisLoading
deuseSWR
isValidating
: identique àisValidating
deuseSWR
mutate
: identique à la fonctionmutate
deuseSWR
, mais manipule le tableau de donnéessize
: le nombre de pages qui seront récupérées et retournéessetSize
: définit le nombre de pages qui doivent être récupérées
Exemple 1: API paginée basée sur l'index
Pour une API basée sur l'index normal :
GET /users?page=0&limit=10
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
]
// Une fonction pour obtenir la clé SWR de chaque page,
// sa valeur de retour sera acceptée par `fetcher`.
// Si `null` est retourné, la requête de cette page ne démarrera pas.
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null // atteint la fin
return `/users?page=${pageIndex}&limit=10` // clé SWR
}
function App () {
const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
if (!data) return 'loading'
// On peut maintenant calculer le nombre total d'utilisateurs
let totalUsers = 0
for (let i = 0; i < data.length; i++) {
totalUsers += data[i].length
}
return <div>
<p>{totalUsers} utilisateurs trouvés</p>
{data.map((users, index) => {
// `data` est un tableau de la réponse API de chaque page.
return users.map(user => <div key={user.id}>{user.name}</div>)
})}
<button onClick={() => setSize(size + 1)}>En récupérer plus</button>
</div>
}
La fonction getKey
est la différence majeure entre useSWRInfinite
et useSWR
.
Elle accepte l'index de la page courante, ainsi que les données de la page précédente.
Donc les API de pagination basées sur l'index et le curseur peuvent être supportées facilement.
Aussi data
n'est plus juste une réponse API. C'est un tableau de plusieurs réponses API :
// `data` ressemblera à ça
[
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
],
[
{ name: 'John', ... },
{ name: 'Paul', ... },
{ name: 'George', ... },
...
],
...
]
Exemple 2: API paginée basée sur le curseur
Disons que l'API demande un curseur et retourne le curseur suivant avec les données :
GET /users?cursor=123&limit=10
{
data: [
{ name: 'Alice' },
{ name: 'Bob' },
{ name: 'Cathy' },
...
],
nextCursor: 456
}
On peut changer la fonction getKey
pour:
const getKey = (pageIndex, previousPageData) => {
// atteint la fin
if (previousPageData && !previousPageData.data) return null
// Première page, on n'a pas `previousPageData`
if (pageIndex === 0) return `/users?limit=10`
// ajout du curseur à l'API
return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}
Mode de récupération en parallèle
Merci de mettre à jour vers la dernière version (≥ 2.1.0) pour utiliser cette API.
Le comportement par défaut de useSWRInfinite est de récupérer les données pour chaque page en séquence, car la création de la clé est basée sur les données récupérées précédemment. Cependant, récupérer les données séquentiellement pour un grand nombre de pages peut ne pas être optimal, particulièrement si les pages ne sont pas interdépendantes. En spécifiant l'option parallel
à true
vous permettra de récupérer les pages indépendamment en parallèle, ce qui peut accélérer significativement le processus de chargement.
// parallel = false (default)
// page1 ===> page2 ===> page3 ===> done
//
// parallel = true
// page1 ==> done
// page2 =====> done
// page3 ===> done
//
// previousPageData est toujours `null`
const getKey = (pageIndex, previousPageData) => {
return `/users?page=${pageIndex}&limit=10`
}
function App () {
const { data } = useSWRInfinite(getKey, fetcher, { parallel: true })
}
L'argument previousPageData
de la fonction getKey
devient null
quand vous activez l'option parallel
.
Revalidate Specific Pages
Please update to the latest version (≥ 2.2.5) to use this API.
The default behavior of the mutation of useSWRInfinite
is to revalidate all pages that have been loaded. But you might want to revalidate only the specific pages that have been changed. You can revalidate only specific pages by passing a function to the revalidate
option.
The revalidate
function is called for each page.
function App() {
const { data, mutate, size } = useSWRInfinite(
(index) => [`/api/?page=${index + 1}`, index + 1],
fetcher
);
mutate(data, {
// only revalidate the last page
revalidate: (pageData, [url, page]) => page === size
});
}
Mutation Globale avec useSWRInfinite
useSWRInfinite
stocke toutes les données de page dans le cache avec une clé de cache spéciale avec chaque donnée de page, donc vous devez utiliser unstable_serialize
dans swr/infinite
pour revalider les données avec la mutation globale.
import { useSWRConfig } from "swr"
import { unstable_serialize } from "swr/infinite"
function App() {
const { mutate } = useSWRConfig()
mutate(unstable_serialize(getKey))
}
Comme le nom l'implique, unstable_serialize
n'est pas une API stable, donc nous pourrions la changer dans le futur.
Fonctionnalités avancées
Voici un exemple montrant comment vous pouvez implémenter les fonctionnalités suivantes avec useSWRInfinite
:
- états de chargement
- afficher une UI spéciale si c'est vide
- désactiver le bouton "En récupérer plus" si on atteint la fin
- source de données changeante
- rafraîchir la liste entière