Skip to main content

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} - для рідко відвідуваних сторінок

Швидкий приклад

tsx
// Базовий навбар - 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 живе в JSX і декларативно обробляє кліки. useRouter дає програмний доступ до навігації після асинхронних операцій. redirect() запускається на сервері, до того як клієнт отримав будь-який HTML.

Правило вибору просте. Якщо користувач клікає і переходить - Link. Якщо код вирішує куди перейти після submit або перевірки авторизації - useRouter. Якщо серверний компонент або Server Action потребує редиректу - redirect().

Коли що використовувати

  • Навбар, картки, хлібні крихти, будь-який клікабельний елемент переходу: Link
  • Після відправки форми, пошуку, успішного логіну: useRouter().push()
  • Перевірка авторизації в серверних компонентах, редирект після мутацій: redirect()
  • Маршрути де кнопка «назад» не повинна повертати на поточну сторінку (модалки, логін): пропс replace у Link або router.replace()
  • Рідко відвідувані або глибокі адмін-сторінки: Link з prefetch={false}

Таблиця порівняння

ХарактеристикаLinkuseRouter().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

tsx
'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

tsx
'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

tsx
// 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} /> }
tsx
// 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

tsx
'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

tsx
// Повне перезавантаження сторінки, втрата scroll restoration і кешу <a href="/dashboard">Дашборд</a> // Правильно <Link href="/dashboard">Дашборд</Link>

Звичайний <a> обходить роутер Next.js повністю. Браузер перезавантажує сторінку і викидає весь закешований RSC payload.

Забутий replace при редиректі після логіну

tsx
// push додає /login до history // Кнопка "назад" повертає користувача на форму логіну router.push('/dashboard') // replace перезаписує поточний запис history router.replace('/dashboard') // Або декларативно: <Link href="/dashboard" replace>Продовжити</Link>

Це часта помилка в продакшені. Кнопка «назад» прокручує користувача назад до логін-форми.

Використання useRouter в серверному компоненті

tsx
// Падає в 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.

tsx
// Тільки 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 навігує тільки той сегмент, до якого належить.

Приклади

Навігація дашборду з активними станами

tsx
// 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).

Форма логіну з програмною навігацією

tsx
// 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 після успішного входу. Без цього кнопка «назад» з дашборду поверне користувача назад на форму.

Серверна перевірка авторизації

tsx
// 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 неавторизованої сторінки.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?