Volver al inicio

URL como la fuente del estado Next.js App Router

En Next.js App Router URL se convierte en la única fuente del estado de la página. Server components leen searchParams y forman el contenido antes de renderizar. El formulario del cliente se sincroniza con el historial del navegador sin useEffect extra.

URL controla los datos en Next.js App Router
Advertisement 728x90

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:

Google AdInline article slot
"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:

Google AdInline article slot
// 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:

Google AdInline article slot
"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 navegador
  • router.push() cambia la URL sin desencadenar una solicitud del lado del cliente
  • getFirstQ() 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=phone siempre 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 useState y 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 searchParams antes de renderizar
  • Las entradas del cliente se sincronizan con useSearchParams()
  • Maneje múltiples parámetros usando getAll()

— Editorial Team

Advertisement 728x90

Leer después