Przenoszenie z next/router: Parametry, URL o niskim poziomie szczegółowości oraz błędy 404 w App Routerze
Jak zastąpić Next/router: w jaki sposób params, useParams, powierzchowne aktualizacje URL, nawigacja programowa oraz prawdziwe strony 404 działają w App Routerze Next.js.
Źródła: Next.js routing: the four next/router habits I had to unlearn
Rozwijający aplikacje korzystające z Pages Router często nadal używają instrukcji import { useRouter } from 'next/router'. W projekcie opartym na App Router taka importacja jest myląca: moduł wciąż istnieje, może nawet przeprowadzać sprawdzanie typów, jednak router.query zwraca wartość pustą, a błąd nie jest oczywisty. Nawigacja w App Router (standard od wersji Next.js 13) znajduje się w pakiecie next/navigation, a kilka powszechnych wzorców z Pages Router po prostu już nie istnieje. Ten przewodnik omawia cztery zmiany, z którymi przyjdzie się zmierzyć podczas przenoszenia małego bloga na App Router, wraz z kodem działającym na wersjach Next.js 16.3.1 i React 19 w momencie pisania.
Parametry dynamiczne są przekazywane jako atrybut, a nie przez hook
W Pages Router zwykłym podejściem było uczynienie strony komponentem klienckim i odczytanie router.query.id. W App Router strona jest komponentem serwerowym, chyba że zaznaczono inaczej, a Next.js przekazuje jej właściwość params. Od wersji Next.js 15 params jest obiektem typu Promise, więc komponent musi być async i użyć await, aby odczytać jakąkolwiek wartość.
Strona importuje funkcję notFound, aby mogła przerwać działanie, gdy identyfikator strony nie odpowiada żadnemu wpisowi:
import { notFound } from 'next/navigation'
Poniższy komponent wyszukuje wpis w pamięci tymczasowej i wywołuje notFound() w przypadku nieznanych identyfikatorów.
const POSTS: Record<string, { title: string; body: string }> = {
'hello-world': { title: 'Hello World', body: 'The first post.' },
}export default async function BlogPost({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = POSTS[id]
if (!post) notFound() return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
}
Tutaj nie ma żadnego hooka ani instrukcji 'use client', a wyszukiwanie odbywa się na serwerze przed wysłaniem jakiegokolwiek HTML. Prośba o dostęp do /blog/hello-world w wersji produkcyjnej zwraca tytuł i treść posta przy kodzie statusu 200. Pominięcie await params to najczęstszy błąd podczas migracji do Next.js 15 i nowszych wersji: wartość, którą destrukturyzujemy, to obietnica (promise), a nie identyfikator.
Czytanie segmentu na stronie klienta za pomocą useParams
Rendering typu server-first nie oznacza, że każdy komponent musi działać na serwerze. Formularz komentarza wymaga identyfikatora posta w przeglądarce. W tym celu używa się useParams z biblioteki next/navigation zamiast router.query. Plik zaczyna się od następującej instrukcji:
'use client'
Następnie komponent odczytuje ten segment. Przekazanie argumentu typu do useParams pozwala uzyskać zwrócony wynik o określonym typie:
import { useParams } from 'next/navigation'export default function CommentBox() {
const { id } = useParams<{ id: string }>()
return <form data-post={id}>Commenting on {id}</form>
}
W odróżnieniu od wersji Pages Router, tutaj nigdy nie zwraca się null: segment jest rozstrzygany przed renderowaniem jakiegokolwiek komponentu klienta poniżej. Formularz renderowany wewnątrz strony post zawiera data-post="hello-world" już przy pierwszym narysowaniu, więc nie ma potrzeby stanu ładowania.
Routing płytki staje się zwykłym wywołaniem History API
Pages Router pozwala na użycie router.push(url, as, { shallow: true }), aby zmienić adres URL bez ponownego pobierania danych. App Router nie posiada opcji shallow. Aby zaktualizować adres URL w celu zmiany kolejności sortowania, filtrów lub aktywnej karty bez ponownego uruchamiania komponentów serwerowych, należy samodzielnie wywołać window.history.pushState i odczytać wartość za pomocą useSearchParams. Również w tym przypadku plik stanowi komponent klienta:
'use client'
Kontroler kopiuje obecne parametry, ustawia sort i dodaje nowy ciąg zapytań; newest służy jako rozwiązanie awaryjne:
import { useSearchParams } from 'next/navigation'export default function SortControls() {
const searchParams = useSearchParams()
const active = searchParams.get('sort') ?? 'newest' function setSort(sort: string) {
const next = new URLSearchParams(searchParams.toString())
next.set('sort', sort)
window.history.pushState(null, '', `?${next.toString()}`)
} return (
<div>
<button aria-pressed={active === 'newest'} onClick={() => setSort('newest')}>
Newest
</button>
<button aria-pressed={active === 'top'} onClick={() => setSort('top')}>
Top
</button>
</div>
)
}
Next.js synchronizuje natywne wywołania pushState i replaceState z jego routerem, dzięki czemu useSearchParams widzi nową wartość, a przyciski aktualizują się bez konieczności wysyłki żądania na serwer. Warto znać dwa zagrożenia:
- Komponent, który wywołuje
useSearchParams, powinien znajdować się wewnątrz elementu<Suspense>. Bez niego strona korzysta z renderowania po stronie klienta aż do najbliższego takiego elementu zamiast być renderowana statycznie, a wynik budowania ostrzega o tym. pushStatedodaje wpis do historii za każde kliknięcie. Jeśli przewijanie wstecz po serii zmian sortowania irytuje użytkowników, należy zamiast tego użyćreplaceState.
Nawigacja programowa nadal wykorzystuje useRouter, z nowej ścieżki
Gdy nawigacja musi przebiegać przez router, na przykład po wysłaniu formularza i przekierowaniu użytkownika do domowej strony, useRouter pozostaje odpowiednim narzędziem. Zmienia się jedynie ścieżka importu. Potrzebuje on również określonej dyrektywy:
'use client'
A sama funkcja wywołania pozostaje bez zmian:
import { useRouter } from 'next/navigation'export default function GoHome() {
const router = useRouter()
return <button onClick={() => router.push('/')}>Home</button>
}
router.push('/') działa tak samo jak wcześniej; cały problem leży w linii importu.
Spersonalizowane strony 404 to plik wysyłający rzeczywisty kod stanu 404
Stara konwencja pliku pages/404.js została zastąpiona plikiem app/not-found.tsx, który jest renderowany za każdym razem, gdy wywołana zostanie funkcja notFound(), w tym również z komponentu serwerowego opisanego w pierwszym rozdziale. Strona zawiera link powrotny na stronę główną, dlatego importuje element Link:
import Link from 'next/link'
Sam komponent to zwykła struktura markup:
export default function NotFound() {
return (
<main>
<h1>404 — no such post</h1>
<Link href="/">Back to the index</Link>
</main>
)
}
Kod stanu ma znaczenie: delikatny błąd 404, strona wyglądająca na błąd serwowana z kodem 200, może zaszkodzić indeksowaniu w wyszukiwarkach, ponieważ boty traktują ją jako rzeczywisty treść. Żądanie nieistniejącego wpisu, np. /blog/nope, zwraca błąd HTTP 404 wraz z komunikatem „taki wpis nie istnieje”. Funkcja notFound() ustawia właśnie taki kod stanu, dzięki czemu to, co widzą boty, jest identyczne z tym, co widzą użytkownicy.
Niech wynik budowania potwierdzi migrację
Gdy migracja zostanie zakończona, tabela tras wyświetlana przez next build to najszybszy sposób na sprawdzenie rezultatu:
Route (app)
┌ ○ /
├ ○ /_not-found
└ ƒ /blog/[id]
Znak ƒ wskazuje, że /blog/[id] jest renderowany dynamicznie, ponieważ odczytuje parametry, natomiast ○ oznacza statyczne ścieżki indeksowe oraz te, których nie można znaleźć. Żaden z kodów nie polega na next/router, router.query ani na ustawieniu shallow: true. Aby dowiedzieć się, co uruchamia się gdzie, zapoznaj się z informacjami na temat umiejscowienia linii serwer-klient w stronie App Router.
Główne wnioski
- Najpierw poszukaj w bazie kodu wyrażenia
from 'next/router'; każde takie wystąpienie odnosi się do jednej z tych czterech modyfikacji. - Dynamiczne segmenty odczytuj z właściwości
paramsw komponentach serwerowych oraz z funkcjiuseParamsw komponentach klienckich.
shallow: true przez history.pushState lub replaceState w połączeniu z useSearchParams, umieszczone wewnątrz <Suspense>.useRouter do rzeczywistych nawigacji, importowanego z next/navigation.app/not-found.tsx wraz z notFound(), aby uzyskać prawdziwy stan 404 zamiast łagodnego 404.Literatura pokrewna
- Gdzie pociągnąć linię serwer-klient w stronie app-router w Next.js — praktyczny model mentalny dla komponentów serwerowych React: co działa gdzie, jak strona posta na blogu dzieli się na części serwerową i kliencką oraz jakie zasady importu należy stosować.
- Budowanie odpornej strony detalu filmu z Next.js App Router — Dowiedz się, jak poprawnie pobierać i przechowywać dane z API OMDB w Next.js App Router przy użyciu asynchronicznych komponentów serwerowych, parametrów awaited oraz prawidłowego obsługi błędu 404.
- SEO techniczne w Next.js App Router: metadane, sitemapy i JSON-LD — Poznaj, jak wspólne narzędzia do metadanych, domyślne ustawienia układu strony, plik robots.ts, dynamiczny sitemap, poprawne JSON-LD oraz audyty stron tworzą solidną bazę SEO dla aplikacji Next.js.
- Jedyny źródło prawdy dla dróg wyświetlania w aplikacjach Next.js — Dowiedz się, jak typowany centralny moduł dróg zastępuje rozproszone ścieżki tekstowe w Next.js, dzięki funkcjom pomocniczym do dynamicznych segmentów i grupowanych eksportów dróg.
- Poprawne rysowanie granicy ‘use client’ w App Routerze Next.js — Dowiedz się, co faktycznie oznacza dyrektywa ‘use client’, dlaczego oznaczanie kontenerów powoduje zwiększenie rozmiaru plików, oraz jak komponenty liściowe i sloty dziecięce utrzymują obliczenia serwera na serwerze.
- Pułapki przy aktualizacji Next.js 16: Defaulty szyn paralnych i middleware do proxy — Dowiedz się, dlaczego szyny paralne w Next.js 16 wymagają pliku default.js, jak bezpiecznie przeprowadzić przeniesienie z middleware.ts na proxy.ts oraz co sprawdzić przed potwierdzeniem ukończenia aktualizacji.