Les search params URL sont l’outil de state management le plus sous-utilisé des applications React. Ils survivent aux refreshs de page, sont partageables via des liens, et sont indexés par les moteurs de recherche. Mais travailler avec URLSearchParams directement est pénible : tout est une string, il n’y a pas de validation, et synchroniser avec le state React demande du boilerplate. nuqs fait des search params des citoyens de première classe dans Next.js avec des parsers typés, des hooks réactifs et le support des server components.
Des search params typés avec des parsers
nuqs fournit des parsers intégrés qui convertissent les strings URL en valeurs typées. Plus de parseInt manuels ou de vérifications === 'true' éparpillées dans les composants.
import { parseAsString, parseAsInteger, parseAsStringEnum, useQueryStates } from 'nuqs';
const statusOptions = ['all', 'active', 'archived'] as const;
export default function ProjectsPage() {
const [filters, setFilters] = useQueryStates({
search: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
status: parseAsStringEnum(statusOptions).withDefault('all'),
});
return (
<div>
<input
value={filters.search}
onChange={(e) => setFilters({ search: e.target.value })}
placeholder="Rechercher un projet..."
/>
<select
value={filters.status}
onChange={(e) => setFilters({ status: e.target.value, page: 1 })}
>
{statusOptions.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</div>
);
}
useQueryStates regroupe plusieurs params dans un seul objet state. Mettre à jour un param peut en réinitialiser un autre dans le même appel, comme remettre page à 1 quand on change un filtre. L’URL reste synchronisée automatiquement, et chaque valeur est correctement typée.
Support des server components
nuqs fonctionne dans les server components via son utilitaire createSearchParamsCache. Ça permet de lire et valider les search params côté serveur, évitant les mismatches d’hydratation client et permettant le data fetching basé sur l’état de l’URL.
import { createSearchParamsCache, parseAsString, parseAsInteger } from 'nuqs/server';
export const searchParamsCache = createSearchParamsCache({
search: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
});
import { searchParamsCache } from '@/lib/search-params';
export default async function ProjectsPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[]>>;
}) {
const { search, page } = searchParamsCache.parse(await searchParams);
const projects = await db.project.findMany({
where: search ? { name: { contains: search } } : undefined,
skip: (page - 1) * 20,
take: 20,
});
return <ProjectList projects={projects} />;
}
Le serveur lit les params typés directement depuis l’URL sans JavaScript client. La requête base de données utilise des valeurs validées et typées. Pas de gymnastique parseInt(searchParams.page || '1').
Mises à jour shallow sans re-renders
Par défaut, nuqs met à jour l’URL via history.pushState sans déclencher de navigation Next.js. Ce qui signifie que changer un search param ne relance pas les server components et ne cause pas de transition de page complète. L’URL se met à jour, le state client se met à jour, et rien d’autre ne se passe.
import { useQueryState, parseAsString } from 'nuqs';
export function SearchInput() {
const [search, setSearch] = useQueryState(
'search',
parseAsString.withDefault('').withOptions({ shallow: true, throttleMs: 300 })
);
return (
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Filtrer..." />
);
}
L’option throttleMs debounce les mises à jour d’URL pour que taper vite ne flood pas l’historique du navigateur. Combiné avec shallow: true, ça donne un input de recherche qui met à jour l’URL en temps réel sans aller-retour serveur ni layout shifts.
Parsers custom pour du state complexe
Au-delà des parsers intégrés, nuqs supporte des serializers custom pour n’importe quelle structure de données. Plages de dates, tableaux et objets imbriqués peuvent tous vivre dans l’URL avec un typage correct.
import { createParser } from 'nuqs';
const parseAsDateRange = createParser({
parse: (value: string) => {
const [from, to] = value.split(',');
if (!from || !to) return null;
return { from: new Date(from), to: new Date(to) };
},
serialize: ({ from, to }) =>
`${from.toISOString().split('T')[0]},${to.toISOString().split('T')[0]}`,
});
L’URL devient ?range=2024-01-01,2024-03-31 au lieu de gérer deux params séparés. Le parser valide l’entrée et renvoie null pour les valeurs malformées, ce qui fallback sur la valeur par défaut.
Conclusion
nuqs transforme les search params URL d’une corvée de manipulation de strings en une vraie couche de state management. Les parsers typés préviennent les erreurs runtime, le support server components permet un filtrage SSR-friendly, et les mises à jour shallow gardent l’UI réactive. Pour toute application Next.js avec des filtres, de la pagination ou des tables triables, c’est la façon la plus propre de garder le state dans l’URL là où il doit être.