Пераход з next/router: параметры, паверхневыя URL-адрэсы і станы 404 у App Router
Как заменіць Next/router: як функцыя params, useParams, апдэты URL у поверхневым режыме, програмна навігацыя і справжнія сторунки 404 працуюць у App Router працэйкі Next.js.
Крыніцы: Next.js routing: the four next/router habits I had to unlearn
Разработчыкі, які працуюць з Pages Router, частаўсь лягкае праганяюцца да выкліку import { useRouter } from 'next/router'. У проектах на App Router такій імпорт є вводзячым у глухае: модуль даскладжваецца, можа нават праходзіць перапрацоўку типаў, але router.query вяртаецца порожнім, і баг не ўсё так проста выяўляецца. Навігацыя ў App Router (стандартны спосаб з Next.js 13) знаходзіцца ў пакете next/navigation, а калькі звычных практык Pages Router проста больш не існуе. У гідзе рассказваецца пра чатыры змены, якія вы зустрэнете пад час переносу маленькаг блога на App Router, з кодам, який працуе пад Next.js 16.3.1 і React 19 на момент напісання.
Дынамічныя параметры прыходзяць як атрыбут, а не через хук
У Pages Router звычным падходам было прыемкаваць сторанку як кліентскі компонент і чытаць router.query.id. У App Router сторанка ўважаецца серверскім компонентам, якщо толькі не зазначана інша рэч, а Next.js перадае ёй прыемку params. З Наступнага Next.js 15 params ўжо є Promise, таму компонент павінен быць async і выкарыстоўваць await, прытаму чытаючы які-небудзь значэння.
Старанка імпортуе notFound, ўбачлівая можлівасць негайна паўтарыцца, калі ідэнтыфікатор сторанкі не падходзіць ні да адной пасты:
import { notFound } from 'next/navigation'
Наведзены выкладак шукае пасту ў кэшованай мапе і вызывае notFound() для невядомых ідэнтыфікатораў.
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>
)
}
Ёсць ніякога хука, ніякай дырэктывы 'use client' тут, і пошук адбываецца на серверы пры перадачы будь-яго HTML. Запрос да /blog/hello-world у версіі для рэальнага выкарыстоўвання вяртае заглавле і тэкст паспісу з статусам 200. Ігнораванне await params — гэта найчастэйшая памылка пад час міграцыі на Next.js 15 і новэйшыя версіі: значэнне, якое вы адгрупаваеце, ёсць праміс, а не id.
Чытанне сегмента на кліянцы за дапамою useParams
Рэндарынг спачатку на серверы не значыць, што кожны компонент працуе на серверы. Форма для комментараў патрэбна ідэнтыфікатор паспісу ў браўзеры. Для гэтага вы вжываеце useParams з пакета next/navigation у замену на router.query. Файл пачынаецца з дырэктывы:
'use client'
Потым компонент чытае гэты сегмент. Перадача аргумента типу да useParams дазволяе отрымаць рэзультат з адзначаным типам:
import { useParams } from 'next/navigation'export default function CommentBox() {
const { id } = useParams<{ id: string }>()
return <form data-post={id}>Commenting on {id}</form>
}
У працоўніку Pages Router ён ніколі не вяртае null: сегмент адначасова вырашваецца раней, чым будзе атрыбутаваны які-небудзь кліентскі компонент. Форма, якая атрыбутуецца ўнутрь сторонніцы, з самага пачатку мае значэнне data-post="hello-world", таму няма патрэбы ў стане загрузкі.
Паверхневая навігацыя становіцца звычным вызывам History API
У Pages Router можна выкарыстоўваць router.push(url, as, { shallow: true }), каб змяніць URL без падтрымкі дадзеных занова. У App Router няма опцыі shallow. Ёк хоча б для змены порядку сортавання, фільтраў чы актывае вкладка без перзапуску серверных компонентаў, трэба самі выклікаць window.history.pushState і чытаць значэнне праз useSearchParams. Знову ж такі, файл являе сабою кліентскі компонент:
'use client'
Контроллер копіюе текущыя параметры, задае значэнне для sort і дадзея новы шаблон запиту; newest выступае як альтэрнатыва:
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 сінхронізуе власныя вызовы pushState і replaceState зі своім рутэрам, таму useSearchParams бачыць новае значэнне і кантролі зменяюцца без неабяжнага запуску на серверы. Є два важлівых моменты, якія трэба знать:
- Компонент, які выкарыстоўвае
useSearchParams, павінен знаходзіцца ў межах<Suspense>. Без яго сторанка будзе адрасавацца для рэндарынгу на кліентскай стороне да найбліжэйшага<Suspense>, а не будзе статычна адрасавацца, пры чым у рэзультатах будовы будзе паведамленне пра гэта. pushStateдадзеяе ентрыю ў історыю за кожны клік. Якщо вярнуцца назад пасля серыі змян сортавання можа раздражваць корыстувальнікаў, лепш выкарыстоўвацьreplaceState.
Програмавыя способы навігацыі яшчэ выкарыстоўваюць useRouter з новага шляху
Калі навігацыя павінна адбывацца через рутэр, напрыклад, калі пасля адправы формы трэба перадаць пользователя на галоўную сторону, useRouter застаецца неабходным інструментам. Зменяецца толькі шлях імпорту. Таксама патрэбна дыректыва:
'use client'
А сам вызов застаецца незменным:
import { useRouter } from 'next/navigation'export default function GoHome() {
const router = useRouter()
return <button onClick={() => router.push('/')}>Home</button>
}
router.push('/') працюе як ранейш; вся проблема крыўчыця ў рэчы імпорту.
Адзінакавыя сторонкі 404 — это файл, який выдае справжні статус 404
Старая практыка pages/404.js заменяецца файлам app/not-found.tsx, які атрыбутуецца кожны раз, калі вызываецца notFound(), укладзены ў серверны компонент з першага раздзела. Сторанка павяртаеся на галоўную, таму ёй імпортуецца Link:
import Link from 'next/link'
Сам компонент — це звычайны маркап:
export default function NotFound() {
return (
<main>
<h1>404 — no such post</h1>
<Link href="/">Back to the index</Link>
</main>
)
}
Код стану мае значэнне: „мягкі“ код 404, сторанка, яка выглядае як адзін з бяспекавых кодоў, але выдаецца з кодам 200, можа нашкодзіць індексаванню у пошуку, адколі боты спрацоўваюць яе як справжній контэнт. Запыт пра відсутняю пастку, напрыклад /blog/nope, вяртае HTTP 404 разам з тэкстам „такой пастка не існуе“. Функцыя notFound() задае гэты стан, таму тое, што бачыць боты, падтрымлівае тое, што бачаць корыстнікі.
Дазвольце рэзультатам запуску падтвердзіць міграцыю
Калі міграцыя завершана, табліца маршрутаў, якая выдрукоўваецца праз next build, — гэта найшырэчы спосаб пераканацца ў рэзультатах:
Route (app)
┌ ○ /
├ ○ /_not-found
└ ƒ /blog/[id]
Знак ƒ показвае, што /blog/[id] адрадзіваецца дынамічна, таму што ён чытае параметры, тады как знак ○ пазначае статычныя індэксы тарактовых маршрутаў. Ніяны з кода не выкарыстовае next/router, router.query чы shallow: true. Чым кераваць, што будзе выканана дзе, пазнайце ў інструкцыях па тым, дзе павінна быць лінія сервер-кліент у сторонцы App Router.
Ключовыя выводы
- Спачатку шукайце ў кодбазе
from 'next/router'; кожны такі знаходкі паўтарае адну з чатырох перамен. - Дынамічныя фрагменты чытайце з атрыбута
paramsу серверных компонентах, а таксама зuseParamsу кліентскіх компонентах.
shallow: true на history.pushState або replaceState разам з useSearchParams, які будуць увукрануты ў <Suspense>.useRouter, які імпортуецца з next/navigation.app/not-found.tsx з notFound(), каб атрымаць справжні статус 404, а не «мягкі» 404.