Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP6 min czytania1340 słów

Ź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.
  • pushState dodaje 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 params w komponentach serwerowych oraz z funkcji useParams w komponentach klienckich.
  • Zastąp shallow: true przez history.pushState lub replaceState w połączeniu z useSearchParams, umieszczone wewnątrz <Suspense>.
  • Zachowaj useRouter do rzeczywistych nawigacji, importowanego z next/navigation.
  • Użyj app/not-found.tsx wraz z notFound(), aby uzyskać prawdziwy stan 404 zamiast łagodnego 404.
  • Literatura pokrewna