Zpět na domů

URL jako zdroj stavu Next.js App Router

V Next.js App Router se URL stává jediným zdrojem stavu stránky. Serverové komponenty čtou searchParams a formují obsah před renderem. Klientský formulář se synchronizuje s historií prohlížeče bez zbytečných useEffect.

URL řídí data v Next.js App Router
Advertisement 728x90

URL jako jediný zdroj stavu v Next.js App Router

V Next.js App Router se URL stává klíčovým faktorem pro stav stránky. Místo synchronizace lokálního useState s adresním řádkem čte serverová stránka searchParams a okamžitě generuje obsah. Tím se odstraní nesrovnalost mezi vstupním polem, daty a navigací.

Při hledání produktů funguje schéma takto: URL obsahuje ?q=phone → server extrahuje parametr → provede požadavek na API → vyrenderuje připravený seznam. Zpět/před a přímé odkazy se chovají předvídatelně.

Typické problémy klientského přístupu

Klasický React kód s useEffect vytváří několik zdrojů pravdy:

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>
    </>
  );
}

Následky:

  • Přímý odkaz neobnoví stav
  • Aktualizace stránky resetuje data
  • Zpět/dopředu porušuje UX
  • Ruční synchronizace vstupu a URL

Serverová model: data před renderováním

Serverový komponent čte searchParams na serveru a načítá data před hydratací:

  • URL uchovává parametry filtrování
  • Server analyzuje searchParams
  • Provede fetch s cache
  • UI se vykreslí z připravených propů

Univerzální datový layer pro produkty:

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(`DummyJSON error: ${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 },
  });
}

Logika hledání je izolovaná, stránka zůstává čistá.

Implementace serverové stránky

// 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>Produkty</h1>
      <GoodsSearchBar initialQuery={q} />

      {isEmpty ? (
        <div>
          <h2>Nic nenalezeno</h2>

          {hasFilter ? (
            <p>Pro dotaz {q} nejsou žádné produkty</p>
          ) : (
            <p>Seznam je prázdný</p>
          )}

          <Link href="/goods">Otevřít všechny produkty</Link>
        </div>
      ) : (
        <GoodsGridMotionClient products={data.products} />
      )}
    </div>
  );
}

URL plně určuje obsah stránky.

Klientské hledání s synchronizací

Vstupní pole zůstává klientské, ale po potvrzení mění pouze URL:

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="např. phone"
      />

      <button type="submit" disabled={!canSearch}>
        Najít
      </button>

      <button type="button" onClick={onReset}>
        Vymazat
      </button>
    </form>
  );
}

Klíčové body:

  • useSearchParams() synchronizuje pole s historií
  • router.push() mění URL bez klientského fetchu
  • getFirstQ() zpracovává duplikáty parametrů

Výhody architektury

  • Jediný zdroj pravdy: URL definuje stav
  • Přímé odkazy: /goods?q=phone vždy dá stejný výsledek
  • Navigace: Zpět/dopředu obnoví přesný stav
  • Serverové vykreslování: Data se načítají před hydratací
  • Ladění: Stav je viditelný v adresním řádku

Kdy URL nepotřebujeme

Parametry vhodné pro URL:

  • Hledací dotazy
  • Řazení a filtry
  • Stránkování
  • Režimy zobrazení

Místní stav:

  • Dropdowny/modalní okna
  • Animace
  • Dočasný vstup
  • Hover/fokus

Tři kritické chyby

  • Dvojí stav bez synchronizace useState + URL
  • Klientský fetch ve serverové stránce přes useEffect
  • Vstup jako hlavní zdroj místo URL po odeslání

Co je důležité:

  • URL by mělo definovat data stránky v App Router
  • Serverové komponenty čtou searchParams před vykreslením
  • Klientské vstupní pole se synchronizuje s useSearchParams()
  • Mnohonásobné parametry zpracovávejte prostřednictvím getAll()

— Editorial Team

Advertisement 728x90

Číst dál