Next/link та навігація в Next.js
next/link - це React-компонент, який керує клієнтською навігацією в Next.js: перехоплює кліки по посиланнях, запобігає повному перезавантаженню сторінки і попередньо завантажує маршрути ще до того, як користувач натиснув.
Теорія
TL;DR
Link- це розумний<a>: той самий HTML, але без перезавантаження і з автоматичним prefetch при появі в зоні видимостіuseRouter().push()- для програмної навігації після виконання логіки (submit форми, відповідь API)redirect()працює на сервері, до того як HTML дійде до клієнта, без'use client'useRouterзnext/navigation(App Router) іnext/router(Pages Router) - різні хуки з різними API- За замовчуванням
Linkробить prefetch при появі в зоні видимості;prefetch={false}- для рідко відвідуваних сторінок
Швидкий приклад
// Базовий навбар - pages/index.tsx
import Link from 'next/link'
export default function Nav() {
return (
<nav>
{/* Prefetch /about коли посилання потрапляє у зону видимості */}
<Link href="/about">Про нас</Link>
{/* Вимикаємо prefetch для рідко відвідуваних сторінок */}
<Link href="/admin/settings" prefetch={false}>Налаштування</Link>
</nav>
)
}
// Клік по "Про нас" змінює контент без перезавантаженняУ DOM Link рендериться як звичайний <a>. Різниця в тому, що відбувається при кліку: Next.js перехоплює подію, викликає event.preventDefault() і передає навігацію роутеру замість браузера.
Link проти useRouter проти redirect
Три інструменти, три контексти. Link живе в JSX і декларативно обробляє кліки. useRouter дає програмний доступ до навігації після асинхронних операцій. redirect() запускається на сервері, до того як клієнт отримав будь-який HTML.
Правило вибору просте. Якщо користувач клікає і переходить - Link. Якщо код вирішує куди перейти після submit або перевірки авторизації - useRouter. Якщо серверний компонент або Server Action потребує редиректу - redirect().
Коли що використовувати
- Навбар, картки, хлібні крихти, будь-який клікабельний елемент переходу:
Link - Після відправки форми, пошуку, успішного логіну:
useRouter().push() - Перевірка авторизації в серверних компонентах, редирект після мутацій:
redirect() - Маршрути де кнопка «назад» не повинна повертати на поточну сторінку (модалки, логін): пропс
replaceуLinkабоrouter.replace() - Рідко відвідувані або глибокі адмін-сторінки:
Linkзprefetch={false}
Таблиця порівняння
| Характеристика | Link | useRouter().push() | redirect() |
|---|---|---|---|
| Де виконується | Клієнт (JSX) | Клієнт (обробник події) | Сервер (компонент/дія) |
| Prefetch | Автоматично | Ні | N/A |
| Додає до history | Так, за замовчуванням | Так | N/A |
Потребує 'use client' | Ні | Так | Ні |
| Сценарій | Навігаційний UI | Навігація після логіки | Auth guard, мутації |
Як працює prefetch
Коли Link потрапляє у зону видимості (приблизно три висоти вікна браузера, поведінка Next.js 14), запускається фоновий fetch RSC payload для цього маршруту і результат кешується в пам'яті. Для статичних маршрутів завантажується повний payload. Для динамічних - тільки до найближчого loading.tsx. Результат: переходи відчуваються миттєво, зазвичай менше 200 мс.
На мобільних пристроях або при слабкому з'єднанні забагато одночасних prefetch-запитів може погіршити продуктивність. Для таких випадків краще prefetch={false} і ручний виклик router.prefetch(url) при наведенні або фокусі.
Активні посилання і usePathname
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export function NavLink({
href,
children
}: {
href: string
children: React.ReactNode
}) {
const pathname = usePathname()
return (
<Link
href={href}
className={pathname === href ? 'font-bold text-blue-600' : 'text-neutral-600'}
>
{children}
</Link>
)
}usePathname працює тільки на клієнті, тому компонент потребує 'use client'. Сам Link цього не вимагає.
Методи useRouter
'use client'
import { useRouter } from 'next/navigation'
export function SearchBar() {
const router = useRouter()
function handleSearch(query: string) {
router.push(`/search?q=${query}`) // додає запис до history
}
return <input onChange={(e) => handleSearch(e.target.value)} />
}| Метод | Що робить |
|---|---|
router.push(url) | Перехід до URL, додає запис у history |
router.replace(url) | Перехід без додавання запису до history |
router.back() | Повернутись назад в history |
router.forward() | Перейти вперед в history |
router.refresh() | Оновити поточний маршрут, перезавантажує дані з сервера |
router.prefetch(url) | Вручну запустити prefetch маршруту |
Важливо: у App Router немає router.query. Для параметрів запиту використовуй useSearchParams.
Серверна навігація з redirect
// app/dashboard/page.tsx - Server Component
import { redirect } from 'next/navigation'
import { getUser } from '@/lib/auth'
export default async function DashboardPage() {
const user = await getUser()
if (!user) redirect('/login') // кидає помилку всередині, Next.js перехоплює
if (!user.hasAccess) redirect('/upgrade')
return <Dashboard user={user} />
}// app/actions.ts - Server Action
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await db.post.create({ data: parseForm(formData) })
redirect(`/posts/${post.id}`) // після мутації, без клієнтського JS
}redirect() всередині кидає спеціальну помилку, яку Next.js перехоплює автоматично. return після виклику redirect() не потрібен.
useSearchParams
'use client'
import { useSearchParams } from 'next/navigation'
export function FilterPanel() {
const searchParams = useSearchParams()
const difficulty = searchParams.get('difficulty') // зчитує ?difficulty=hard
return <p>Фільтр: {difficulty || 'всі'}</p>
}useSearchParams - стандартний спосіб читати параметри запиту в клієнтських компонентах App Router. router.query тут немає.
Типові помилки
Використання <a> замість Link в App Router
// Повне перезавантаження сторінки, втрата scroll restoration і кешу
<a href="/dashboard">Дашборд</a>
// Правильно
<Link href="/dashboard">Дашборд</Link>Звичайний <a> обходить роутер Next.js повністю. Браузер перезавантажує сторінку і викидає весь закешований RSC payload.
Забутий replace при редиректі після логіну
// push додає /login до history
// Кнопка "назад" повертає користувача на форму логіну
router.push('/dashboard')
// replace перезаписує поточний запис history
router.replace('/dashboard')
// Або декларативно:
<Link href="/dashboard" replace>Продовжити</Link>Це часта помилка в продакшені. Кнопка «назад» прокручує користувача назад до логін-форми.
Використання useRouter в серверному компоненті
// Падає в runtime. useRouter - тільки клієнтський хук.
export default async function Page() {
const router = useRouter() // Помилка!
}
// Правильно: redirect() на сервері
import { redirect } from 'next/navigation'
export default async function Page() {
const user = await getUser()
if (!user) redirect('/login')
}Плутанина між next/navigation і next/router
Цю помилку я бачив кілька разів - обидва імпорти компілюються без помилок. Проблема вилазить в runtime, коли router.query виявляється undefined в App Router.
// Тільки Pages Router - є router.query
import { useRouter } from 'next/router'
// App Router - немає router.query, використовуй useSearchParams
import { useRouter } from 'next/navigation'
import { useSearchParams } from 'next/navigation'Де зустрічається в реальних проектах
- Vercel dashboard:
Linkдля навігації між проектами і деплоями в сайдбарі - Компоненти shadcn/ui:
Linkразом зusePathnameдля підсвічування активного пункту меню - T3 Stack:
Linkдля захищених маршрутів після авторизації,redirect()в NextAuth callbacks - Сторінки пошуку і фільтрації:
useRouter().push()для оновлення URL після вибору фільтрів
Питання на співбесіді
Q: Яка різниця між push і replace при навігації?
A: push додає новий запис до стеку history браузера, тому кнопка «назад» працює звично. replace перезаписує поточний запис. Використовують replace для редиректів після логіну і модальних флоу, де «назад» не повинен повертати на ту саму сторінку.
Q: Як prefetch поводиться для динамічних маршрутів?
A: Для статичних маршрутів Next.js завантажує повний RSC payload. Для динамічних - тільки до найближчого loading.tsx. Shell рендериться одразу, дані завантажуються вже після переходу.
Q: У чому різниця між useRouter з next/navigation і next/router?
A: next/navigation - для App Router (Next.js 13+), без router.query. next/router - для Pages Router. Помилка компілятора не виникне, але router.query буде undefined в runtime в App Router.
Q: Як обробляти стан завантаження при програмній навігації?
A: App Router автоматично показує loading.tsx при переходах. Для точнішого контролю загорни router.push() в useTransition і використовуй флаг isPending щоб вимкнути кнопку або показати спіннер під час переходу.
Q: Як Link взаємодіє з паралельними маршрутами і intercepting routes?
A: Intercepting routes (конвенція @folder) перевизначають розпізнавання href до того як спрацює prefetch. Паралельні маршрути завантажуються незалежно, тому слот @modal залишається змонтованим поки змінюється основний маршрут. Link навігує тільки той сегмент, до якого належить.
Приклади
Навігація дашборду з активними станами
// components/DashboardNav.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
const navItems = [
{ href: '/dashboard', label: 'Огляд' },
{ href: '/dashboard/analytics', label: 'Аналітика' },
{ href: '/dashboard/users', label: 'Користувачі', skipPrefetch: true },
]
export default function DashboardNav() {
const pathname = usePathname()
return (
<nav className="flex gap-4">
{navItems.map(({ href, label, skipPrefetch }) => (
<Link
key={href}
href={href}
prefetch={skipPrefetch ? false : undefined}
className={pathname === href ? 'font-bold text-blue-600' : 'text-neutral-600'}
>
{label}
</Link>
))}
</nav>
)
}
// Активний пункт виділяється жирним; /users пропускає prefetchПеревірка активного стану - це строге порівняння повного pathname. Для вкладених маршрутів типу /dashboard/analytics/reports потрібно pathname.startsWith(href).
Форма логіну з програмною навігацією
// app/login/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function LoginPage() {
const router = useRouter()
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
const formData = new FormData(e.currentTarget)
const result = await login(formData)
if (result.success) {
// replace щоб кнопка «назад» не повертала на форму логіну
router.replace('/dashboard')
} else {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<input name="password" type="password" />
<button disabled={loading}>
{loading ? 'Входимо...' : 'Увійти'}
</button>
</form>
)
}router.replace тут запобігає потраплянню сторінки логіну в history після успішного входу. Без цього кнопка «назад» з дашборду поверне користувача назад на форму.
Серверна перевірка авторизації
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { getUser } from '@/lib/auth'
import { Dashboard } from '@/components/Dashboard'
export default async function DashboardPage() {
const user = await getUser()
// Виконується на сервері, клієнт не отримує неавторизований HTML
if (!user) redirect('/login')
if (user.plan === 'free') redirect('/upgrade')
return <Dashboard user={user} />
}
// redirect() кидає помилку всередині, Next.js обробляє її автоматично
// Явний return після redirect не потрібенЦей патерн - стандартний auth guard в App Router. Перевірка відбувається до рендеру компонента, 'use client' не потрібен, і клієнт ніколи не отримує HTML неавторизованої сторінки.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.