URL como única fuente de verdad en Next.js App Router
En el Router de Aplicaciones de Next.js, la URL se convierte en la fuente definitiva del estado de la página. En lugar de sincronizar useState local con la barra de direcciones, la página renderizada por el servidor lee searchParams y genera contenido de inmediato. Esto elimina inconsistencias entre campos de entrada, datos y navegación.
Para búsquedas de productos, funciona así: la URL contiene ?q=phone → el servidor extrae el parámetro → realiza una llamada a la API → renderiza una lista para mostrar. La navegación hacia atrás/adelante y los enlaces directos funcionan de forma predecible.
Errores comunes de enfoques del lado del cliente
El código React tradicional usando useEffect crea múltiples fuentes de verdad:
"use client";
import { useEffect, useState } from "react";
export default function GoodsPage() {
const [q, setQ] = useState("");
const [items, setItems] = useState([]);
useEffect(() => {
async function load() {
const res = await fetch(`/api/goods?q=${encodeURIComponent(q)}`);
const data = await res.json();
setItems(data.products);
}
load();
}, [q]);
return (
<>
<input value={q} onChange={e => setQ(e.target.value)} />
<div>{items.map(item => <div key={item.id}>{item.title}</div>)}</div>
</>
);
}
Consecuencias:
- Los enlaces directos no restauran el estado
- Recargar la página reinicia los datos
- Navegar hacia atrás/adelante rompe la experiencia de usuario
- Se requiere sincronización manual entre entrada y URL
Modelo del lado del servidor: datos antes del renderizado
Los componentes del servidor leen searchParams en el servidor y obtienen datos antes de la hidratación:
- La URL almacena los parámetros de filtrado
- El servidor analiza
searchParams - Se ejecuta una solicitud de fetch con caché
- La interfaz se renderiza desde props pre-cargados
Una capa universal de datos para productos:
// src/app/_data/dummyjson.js
const API_BASE = "https://dummyjson.com";
async function fetchJson(url, fetchOptions = {}) {
const res = await fetch(url, fetchOptions);
if (!res.ok) {
const text = await res.text().catch(() => "");
const err = new Error(`Error de DummyJSON: ${res.status} ${res.statusText}. ${text}`);
err.status = res.status;
throw err;
}
return res.json();
}
export async function getProducts({ q = "", limit = 12, skip = 0 } = {}) {
const safeQ = String(q).trim();
const qs = new URLSearchParams({
limit: String(limit),
skip: String(skip),
});
const url = safeQ
? `${API_BASE}/products/search?${qs.toString()}&q=${encodeURIComponent(safeQ)}`
: `${API_BASE}/products?${qs.toString()}`;
return fetchJson(url, {
next: { revalidate: 60 },
});
}
La lógica de búsqueda está aislada: los componentes de página permanecen limpios y enfocados.
Implementando la página del lado del servidor
// src/app/(app)/goods/page.js
import Link from "next/link";
import { getProducts } from "@/app/_data/goodsApi";
import GoodsSearchBar from "@/app/_ui/GoodsSearchBar";
import GoodsGridMotionClient from "@/app/_ui/GoodsGridMotionClient";
export default async function GoodsPage({ searchParams }) {
const sp = await searchParams;
const q = typeof sp?.q === "string" ? sp.q : "";
const data = await getProducts({ q, limit: 12, skip: 0 });
const isEmpty = !data?.products?.length;
const hasFilter = !!String(q || "").trim();
return (
<div>
<h1>Productos</h1>
<GoodsSearchBar initialQuery={q} />
{isEmpty ? (
<div>
<h2>No se encontraron resultados</h2>
{hasFilter ? (
<p>No hay productos que coincidan con "{q}"</p>
) : (
<p>La lista está vacía</p>
)}
<Link href="/goods">Ver todos los productos</Link>
</div>
) : (
<GoodsGridMotionClient products={data.products} />
)}
</div>
);
}
La URL determina completamente el contenido de la página.
Búsqueda del lado del cliente con sincronización de URL
El campo de entrada permanece del lado del cliente, pero solo actualiza la URL tras enviar el formulario:
"use client";
import { useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
function getFirstQ(sp) {
const all = sp.getAll("q");
return all[0] ?? "";
}
export default function GoodsSearchBar({ basePath = "/goods" }) {
const router = useRouter();
const sp = useSearchParams();
const qFromUrl = getFirstQ(sp);
const [value, setValue] = useState(qFromUrl);
useEffect(() => {
setValue(qFromUrl);
}, [qFromUrl]);
function buildHref(nextQ) {
const q = nextQ.trim();
return q ? `${basePath}?q=${encodeURIComponent(q)}` : basePath;
}
function onSubmit(e) {
e.preventDefault();
router.push(buildHref(value));
}
function onReset() {
router.push(basePath);
}
const canSearch = useMemo(() => value.trim().length > 0, [value]);
return (
<form onSubmit={onSubmit}>
<input
value={value}
onChange={e => setValue(e.target.value)}
placeholder="por ejemplo, teléfono"
/>
<button type="submit" disabled={!canSearch}>
Buscar
</button>
<button type="button" onClick={onReset}>
Restablecer
</button>
</form>
);
}
Puntos clave:
useSearchParams()mantiene la entrada sincronizada con el historial del navegadorrouter.push()cambia la URL sin desencadenar una solicitud del lado del clientegetFirstQ()maneja parámetros de consulta duplicados
Beneficios de esta arquitectura
- Fuente única de verdad: la URL define el estado de la página
- Enlaces directos:
/goods?q=phonesiempre devuelve el mismo resultado - Navegación: retroceder/avanzar restaura exactamente el estado
- Renderizado del servidor: los datos cargan antes de la hidratación
- Depuración: el estado es visible en la URL
Cuándo evitar usar parámetros de URL
Buenas candidatas para parámetros de URL:
- Consultas de búsqueda
- Ordenamiento y filtrado
- Paginación
- Modos de visualización
Mejor mantener en estado del cliente:
- Menús desplegables y modales
- Animaciones
- Entradas temporales
- Estados de hover o foco
Tres errores críticos que evitar
- Estado dual sin sincronización: mezclar
useStatey URL sin coordinación - Solicitud del lado del cliente dentro de componentes del servidor mediante
useEffect - Entrada como fuente principal en lugar de URL tras enviar el formulario
Lo más importante:
- La URL debe definir los datos de la página en App Router
- Los componentes del servidor leen
searchParamsantes de renderizar - Las entradas del cliente se sincronizan con
useSearchParams() - Maneje múltiples parámetros usando
getAll()
— Editorial Team
Aún no hay comentarios.