
Comment implémenter des paramètres de recherche avec React Router v.6 ?
Cet article est un work in progress.
Note pour moi même: gérer les null, conclusion, relecture, test IRL, illustration...
Question
J'ai une page affichant une liste filtrable et paginable.
L'UX de cette page est mauvaise: si je consulte un élement de la liste puis que je retourne sur cette liste, je perd mes filtres et retourne à la page 1.
Voici mon composant :
Comment faire ?
import React, { useState } from 'react';
const UsersList = () => {
const [page, setPage] = useState(1)
const [search, setSearch] = useState("")
const [status, setStatus] = useState([])
return (
<>
<h1>Users</h1>
<Filters
searchValue={search}
onSearch={(newSearch) => setSearch(newSearch)}
statusValue={status}
onStatusChange={(newValue) => setStatus(newValue)}
/>
<List/>
<Pagination
page={page}
onChange={(newPage) => setPage(newPage)}
/>
</>
)
}
Réponse
React Router v.6
fournit le hook
useSearchParams
.
TL;DR
Voilà comment procéder
CODE FINAL
Explications
L'idée ici est de remplacer les filtres et la pagination utilisants
le useState
de React par le
useSearchParams
de React Router. Cela va permettre de
gérer l'état (= state) du composant directement dans l'URL.
Ce hook nous retourne un array de 2 valeurs :
searchParams
qui est les paramètres de la localisation en cours, et
setSearchParams
, une fonction qui peut être utilisée
pour mettre à jour ces mêmes paramètres.
Commençons par la pagination.
-
On commence par remplacer
useState
paruseSearchParams
en lui fournissant une valeur de page par défaut. -
Puis on recupère la valeur
page
venant de l'URL. L'URL renvoyant toujours destring
, il faut veiller à convertir la valeur en nombre. -
Et l'on met à jour les paramètres d'URL au moment de la pagination.
import React, { useState } from 'react';
import { useSearchParams } from "react-router-dom";
const UsersList = () => {
const [page, setPage] = useState(1)
const [searchParams, setSearchParams] = useSearchParams({ page: 1 });
...
return (
<>
<h1>Users</h1>
...
<Pagination
page={page}
page={+(searchParams.get("page"))}
onChange={(newPage) => setPage(newPage)}
onChange={(newPage) => setSearchParams(prev => {
prev.set(page: newPage)
return prev
})
}
/>
</>
)
}
Et voilà ! L'URL sera mise à jour à chaque changement de page et sera persistée dans l'URL pour retrouver la même page en revenant sur cette liste.
Vous n'avez plus qu'à décliner ce code sur les autres filtres :
import React, { useState } from 'react';
import { useSearchParams } from "react-router-dom";
const UsersList = () => {
const [searchParams, setSearchParams] = useSearchParams({ page: 1, search: "", status: [] });
return (
<>
<h1>Users</h1>
<Filters
searchValue={search}
searchValue={searchParams.get("search")}
onSearch={(newSearch) => setSearch(newSearch)}
onSearch={(newSearch) => (newPage) => setSearchParams(prev => {
prev.set(search: newSearch)
return prev
}, { replace: true })
}
statusValue={status}
statusValue={searchParams.get("status")}
onStatusChange={(newStatus) => setStatus(newStatus)}
onStatusChange={(newStatus) => setSearchParams(prev => {
prev.set(status: newStatus)
return prev
}, { replace: true }
)}
/>
<Pagination
page={+(searchParams.get("page"))}
onChange={(newPage) => setSearchParams({ page: newPage })}
onChange={(newPage) => setSearchParams(prev => {
prev.set(page: newPage)
return prev
}, { replace: true })
}
/>
</>
)
}
Notez ici le { replace: true }
.
Si plusieurs filtres sont appliqués à la suite, le retour en arrière remontera chaque changement appliqué sur l'URL. Cette option permet d'éviter celà.
Ré-usinage
Ce code fonctionne mais il est très redondant donc nous allons le ré-usiner.
-
On va isoler la récupération des variable en un même endroit, pour une meilleure lecture. Nous pourrions d'ailleurs aussi gérer les "null" de façon plus propre, mais ce n'est pas le sujet de l'article.
-
Puis on créé une fonction
handleFilters()
qui prend une clé et une valeur en paramètre, et qui sera utilisée à chaque mise à jour des paramètres.
import React, { useState } from 'react';
import { useSearchParams } from "react-router-dom";
const UsersList = () => {
const [searchParams, setSearchParams] = useSearchParams({ page: 1, search: "", status: [] });
const page = +(searchParams.get("page"))
const search = searchParams.get("search")
const status = searchParams.get("status")
const handleFilters = (key, value) => {
setSearchParams(
prev => {
prev.set(key, value)
return prev
},
{ replace: true },
)
if (key !== 'page') handleFilters('page', 1)
}
return (
<>
<h1>Users</h1>
<Filters
searchValue={searchParams.get("search")}
searchValue={search}
onSearch={(newSearch) => (newPage) => setSearchParams(prev => {
prev.set(search: newSearch)
return prev
}, { replace: true })
}
onSearch={(newSearch) => handleFilter("search", newSearch)}
onStatusChange={(newStatus) => setSearchParams(prev => {
prev.set(status: newStatus)
return prev
}, { replace: true }
)}
onStatusChange={(newStatus) => handleFilter("status", newStatus)}
/>
<Pagination
page={+(searchParams.get("page"))}
page={page}
onChange={(newPage) => setSearchParams(prev => {
prev.set(page: newPage)
return prev
}, { replace: true })
}
onChange={(newPage) => handleFilters("page", newPage)}
/>
</>
)
}
Si besoin est, pour simuler le comportement du bouton de retour en
arrière natif de votre navigateur, vous pouvez invoquer
navigate(-1)
issu de
useNavigate
, un autre hook de React Router v.6.