Parámetros de búsqueda
Lee y manipula parámetros de consulta de URL con useSearchParams (client) y la prop searchParams (server).
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Server Component -- accede a searchParams como una prop (Promise en Next.js 15+)
type Props = { searchParams: Promise<{ q?: string; page?: string }> };
export default async function Page({ searchParams }: Props) {
const { q, page } = await searchParams;
const results = await search(q, Number(page) || 1);
return <ResultsList results={results} />;
}// Client Component -- hook useSearchParams
"use client";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
export function SearchInput() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams.toString());
if (term) params.set("q", term);
else params.delete("q");
router.push(`${pathname}?${params.toString()}`);
}
return (
<input
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Buscar..."
/>
);
}Cuándo usarlo: Necesitas estado impulsado por URL -- filtros de búsqueda, paginación, ordenamiento -- que deban ser compartibles via URL y sobrevivir a las recargas de página.
Ejemplo funcional
// app/products/page.tsx (Server Component)
import { Suspense } from "react";
import { SearchBar } from "./search-bar";
import { ProductGrid } from "./product-grid";
import { Pagination } from "./pagination";
type Props = {
searchParams: Promise<{
q?: string;
category?: string;
sort?: string;
page?: string;
}>;
};
export default async function ProductsPage({ searchParams }: Props) {
const { q, category, sort, page } = await searchParams;
return (
<main className="max-w-6xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Productos</h1>
<Suspense fallback={<div>Cargando búsqueda...</div>}>
<SearchBar />
</Suspense>
<Suspense
key={`${q}-${category}-${sort}-${page}`}
fallback={<div>Cargando productos...</div>}
>
<ProductResults
query={q}
category={category}
sort={sort ?? "newest"}
page={Number(page) || 1}
/>
</Suspense>
</main>
);
}
async function ProductResults({
query,
category,
sort,
page,
}: {
query?: string;
category?: string;
sort: string;
page: number;
}) {
const { products, totalPages } = await fetchProducts({
query,
category,
sort,
page,
});
return (
<>
<ProductGrid products={products} />
<Pagination currentPage={page} totalPages={totalPages} />
</>
);
}// app/products/search-bar.tsx
"use client";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useTransition } from "react";
import { useDebouncedCallback } from "use-debounce";
export function SearchBar() {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams.toString());
if (term) {
params.set("q", term);
params.set("page", "1"); // resetea a página 1 en búsqueda nueva
} else {
params.delete("q");
}
startTransition(() => {
router.push(`${pathname}?${params.toString()}`);
});
}, 300);
return (
<div className="relative mb-6">
<input
type="search"
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Buscar productos..."
className="w-full border rounded px-4 py-2"
/>
{isPending && (
<span className="absolute right-3 top-2.5 text-gray-400">
Buscando...
</span>
)}
</div>
);
}// app/products/pagination.tsx
"use client";
import { useSearchParams, usePathname } from "next/navigation";
import Link from "next/link";
export function Pagination({
currentPage,
totalPages,
}: {
currentPage: number;
totalPages: number;
}) {
const searchParams = useSearchParams();
const pathname = usePathname();
function createPageUrl(page: number) {
const params = new URLSearchParams(searchParams.toString());
params.set("page", page.toString());
return `${pathname}?${params.toString()}`;
}
return (
<div className="flex gap-2 mt-6">
{currentPage > 1 && (
<Link href={createPageUrl(currentPage - 1)} className="px-3 py-1 border rounded">
Anterior
</Link>
)}
<span className="px-3 py-1">
Página {currentPage} de {totalPages}
</span>
{currentPage < totalPages && (
<Link href={createPageUrl(currentPage + 1)} className="px-3 py-1 border rounded">
Siguiente
</Link>
)}
</div>
);
}Lo que demuestra:
- Leer
searchParamsen el servidor como una Promise (patrón Next.js 15+) - Usar
useSearchParamsen el cliente para leer y actualizar parámetros de consulta - Debouncing de entrada de búsqueda para evitar navegaciones excesivas
- Construir URLs de paginación que preserven parámetros de búsqueda existentes
- Usar una
keyen<Suspense>para volver a triggerear estados de carga en cambios de parámetros
Análisis profundo
Cómo funciona
searchParamsdel lado del servidor: En el App Router, los componentes de página recibensearchParamscomo una prop. En Next.js 15+, esto es unaPromiseque debe ser esperada. Acceder asearchParamsactiva el renderizado dinámico de la ruta.useSearchParamsdel lado del cliente: Devuelve una instanciaURLSearchParamsde solo lectura. Para actualizar, construye un nuevoURLSearchParams, modifícalo, y presiona conrouter.push()orouter.replace().useSearchParamsrequiere un límite de Suspense -- Durante el prerendering estático, los parámetros de búsqueda no están disponibles. Envolviendo el componente en<Suspense>proporciona un fallback mientras los parámetros se hidratan en el cliente.- El estado de URL es parte de la pila de historial del navegador. Usar
router.pushcrea una nueva entrada de historial;router.replacereemplaza la entrada actual (mejor para filtros y ordenamiento).
Variaciones
Usando router.replace para evitar contaminar el historial:
// Bueno para filtros -- el usuario aún puede usar el botón atrás de forma significativa
startTransition(() => {
router.replace(`${pathname}?${params.toString()}`);
});Leyendo un parámetro único con un valor por defecto:
const sort = searchParams.get("sort") ?? "newest";
const page = Number(searchParams.get("page")) || 1;Parámetros multivalor (arrays):
// URL: ?color=red&color=blue
const colors = searchParams.getAll("color"); // ["red", "blue"]
// Configurando múltiples valores
const params = new URLSearchParams();
["red", "blue"].forEach((c) => params.append("color", c));Notas de TypeScript
// Tipo searchParams para Server Component (Next.js 15+)
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
// Helper de parámetros de búsqueda tipado
type ProductFilters = {
q?: string;
category?: string;
sort?: "newest" | "price-asc" | "price-desc";
page?: string;
};
type Props = { searchParams: Promise<ProductFilters> };Trampa común
-
searchParamsactiva el renderizado dinámico de la ruta -- Cualquier página que leasearchParamsen el servidor no puede ser generada estáticamente. Solución: Si quieres una página estática con filtrado del lado del cliente, lee los parámetros solo en Client Components conuseSearchParams. -
useSearchParamssin Suspense causa errores de construcción -- Durante el prerendering,useSearchParamslanza una excepción porque no hay parámetros para leer. Solución: Siempre envuelve componentes que usanuseSearchParamsen un límite<Suspense>. -
Cierre estancado con
searchParams-- En un Client Component, la referenciasearchParamsdeuseSearchParams()se actualiza en la navegación, pero los cierres en manejadores de eventos pueden capturar el valor anterior. Solución: LeesearchParamsen el momento de la llamada dentro del manejador, no fuera de él. -
Los parámetros de array pierden seguridad de tipo --
searchParams.get("color")devuelve solo el primer valor incluso cuando existen múltiples. Solución: UsasearchParams.getAll("color")para parámetros multivalor. -
searchParamses una Promise en Next.js 15+ -- Desestructurarlo directamente sinawaitte da un objeto Promise. Solución: Siempreconst { q } = await searchParamsen Server Components.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
useState (solo cliente) | El estado es efímero y no necesita sobrevivir a recargas | El estado debe ser compartible via URL |
Segmentos de ruta dinámicos ([slug]) | El parámetro define la identidad del recurso | El parámetro es un filtro o modificador |
| Cookies | El estado debe persistir entre sesiones pero no aparecer en la URL | El estado debe ser visible y compartible |
Librería nuqs | Quieres parámetros de búsqueda con seguridad de tipo y validación | El useSearchParams incorporado es suficiente |
| Zustand con sincronización de URL | Estado multíparámetro complejo con valores derivados | Parámetros de URL simples de clave-valor |
FAQs
¿Por qué searchParams es una Promise en Server Components de Next.js 15+?
- En Next.js 15+,
searchParamses unaPromiseque debe ser esperada antes de acceder a las propiedades - Desestructurar sin
awaitte da un objeto Promise, no los valores - Esto se alinea con el modelo de Server Component asincrónico
¿Leer searchParams en el servidor previene la generación estática?
- Sí, acceder a
searchParamsen un Server Component activa el renderizado dinámico de la ruta - La página no puede ser prerendering porque los parámetros de búsqueda solo se conocen en el momento de la solicitud
- Si quieres una página estática, lee los parámetros solo en Client Components con
useSearchParams
¿Por qué useSearchParams requiere un límite de Suspense?
- Durante el prerendering estático, los parámetros de búsqueda no están disponibles
- Sin un límite
<Suspense>, la construcción falla porqueuseSearchParamslanza una excepción - El fallback de Suspense se muestra mientras los parámetros se hidratan en el cliente
¿Cuál es la diferencia entre router.push y router.replace para parámetros de búsqueda?
router.pushcrea una nueva entrada de historial del navegador (el usuario puede presionar atrás)router.replacereemplaza la entrada de historial actual (mejor para filtros y ordenamiento)- Usa
replacecuando los cambios de parámetros frecuentes contaminarían la pila de historial
¿Cómo manejas parámetros de búsqueda multivalor (ej. ?color=red&color=blue)?
// Leyendo múltiples valores
const colors = searchParams.getAll("color"); // ["red", "blue"]
// Configurando múltiples valores
const params = new URLSearchParams();
["red", "blue"].forEach((c) => params.append("color", c));searchParams.get("color")solo devuelve el primer valor
¿Cómo reseteas la paginación a página 1 cuando la consulta de búsqueda cambia?
const handleSearch = (term: string) => {
const params = new URLSearchParams(searchParams.toString());
if (term) {
params.set("q", term);
params.set("page", "1"); // resetea a página 1
} else {
params.delete("q");
}
router.push(`${pathname}?${params.toString()}`);
};¿Cómo escribes el tipo de searchParams para una página Server Component en TypeScript?
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{
[key: string]: string | string[] | undefined;
}>;
};- Los valores pueden ser
string,string[], oundefined
¿Cuál es la trampa de cierre estancado con useSearchParams?
- Los cierres en manejadores de eventos pueden capturar una referencia antigua de
searchParams - El objeto
searchParamsse actualiza en la navegación, pero el cierre no lo vuelve a capturar - Lee
searchParamsen el momento de la llamada dentro del manejador para obtener el valor más reciente
¿Cómo usas una clave key de Suspense para volver a triggerear estados de carga cuando los parámetros cambian?
<Suspense
key={`${q}-${category}-${sort}-${page}`}
fallback={<div>Cargando...</div>}
>
<ProductResults query={q} sort={sort} page={page} />
</Suspense>- Cambiar la
keydesmonta y remonta el límite de Suspense, mostrando el fallback de nuevo
¿Cuándo debes usar searchParams versus segmentos de ruta dinámicos como [slug]?
- Usa segmentos dinámicos (
[slug]) cuando el parámetro define la identidad del recurso (ej./products/shoes) - Usa
searchParamscuando el parámetro es un filtro o modificador (ej.?sort=price&page=2) - Los parámetros de búsqueda son opcionales; los segmentos dinámicos son partes requeridas de la URL
¿Cómo creas un helper de parámetros de búsqueda tipado para mejor seguridad de tipo?
type ProductFilters = {
q?: string;
category?: string;
sort?: "newest" | "price-asc" | "price-desc";
page?: string;
};
type Props = { searchParams: Promise<ProductFilters> };- Esto reduce los valores posibles y proporciona autocompletar en tu IDE
Relacionado
- Fetching -- Usando parámetros de búsqueda para impulsar consultas del lado del servidor
- Cookies & Headers -- Otras formas de acceder a datos de solicitud
- Client Components -- Donde vive
useSearchParams - Static vs Dynamic -- Cómo searchParams afecta el modo de renderizado