Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Next/link та навігація в Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`next/link`** - основний компонент навігації в Next.js: рендериться як `<a>`, але виконує перехід на клієнті без перезавантаження сторінки. ```tsx <Link href="/dashboard">Дашборд</Link> // Програмна навігація: const router = useRouter() router.replace('/dashboard') // Серверна: redirect('/login') ``` **Ключове:** `Link` для кліків у UI, `useRouter` після виконання логіки, `redirect()` у серверних компонентах.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`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 проти 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 ```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 неавторизованої сторінки.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.