Асинхронні cookies(), headers() та params у Next.js 16: повний гід із міграції (2026)
cookies(), headers(), draftMode() і params у Next.js 16 стали асинхронними. Повний гід міграції: codemod, типізація через next typegen, React.use() у Client Components і практичні патерни auth, i18n, темізації.
У Next.js 16 функції cookies(), headers(), draftMode(), а також props params і searchParams стали асинхронними. Тепер вони повертають Promise, який потрібно await-ити перед використанням. Синхронний доступ, що ще працював з попередженнями у Next.js 15, у 16-й версії видалений остаточно. Чесно кажучи, я обпікся на цьому вже двічі під час апгрейду двох проєктів, тож у цьому гіді покажу повний шлях міграції з Next.js 15 на 16: як запустити офіційний codemod, що він пропускає, як типізувати нові Promise-signatures і як правильно «розпакувати» ці API у Client Components через React.use().
cookies(), headers() та draftMode() у Next.js 16 повертають Promise. Доступ без await викликає runtime-помилку у production.
Props params і searchParams у page.tsx, layout.tsx, route.ts, opengraph-image.tsx тепер також є Promise-типами.
Офіційний codemod npx @next/codemod@canary next-async-request-api покриває більшість випадків, але залишає TODO-коментарі там, де не може автоматично трансформувати код.
Утиліта npx next typegen генерує глобальні типи PageProps, LayoutProps, RouteContext. Використовуйте їх замість ручних інтерфейсів.
У Client Components використовуйте React.use(promise) для синхронного доступу, оскільки await у клієнтських функціях не працює.
Ручний grep params\., cookies\(\), headers\(\) після codemod ловить те, що автоматика пропустила.
Що змінилося у Next.js 16
До Next.js 14 функції з next/headers та props маршрутів були синхронними: ви писали const store = cookies() і одразу отримували об'єкт. У Next.js 15 команда Vercel почала переводити ці API на асинхронні. Синхронний доступ ще працював, але з console.error у dev-режимі та deprecation-попередженням. У Next.js 16, який вийшов 21 жовтня 2025 року, синхронний шлях видалений повністю. Спроба звернутись до params.id напряму (без попереднього await params) кине runtime-помилку Error: Route "/products/[id]" used params.id. params should be awaited before using its properties.
Це стосується таких API:
cookies(): читання cookies запиту у Server Components, Route Handlers та Server Actions;
headers(): читання HTTP-заголовків запиту;
draftMode(): перевірка стану Draft/Preview Mode;
params у page.tsx, layout.tsx, route.ts, default.tsx, opengraph-image.tsx, twitter-image.tsx, icon.tsx, generateMetadata;
searchParams у page.tsx.
Ключова причина полягає у Partial Prerendering (PPR) та dynamicIO. Next.js має вміти запускати статичну частину компонента ще до того, як стане відомий сам запит, і зупинятися лише на межі, де ви фактично звертаєтесь до динамічних даних. Promise дозволяє позначити цю межу декларативно.
Чому cookies() і headers() стали асинхронними?
Асинхронна модель, це не косметична зміна. Вона напряму пов'язана з двома ключовими фічами Next.js 16: Partial Prerendering (PPR) та Cache Components. У PPR один і той самий маршрут містить статичний shell, який рендериться під час білду, та динамічні «дірки», які підвантажуються на кожен запит. Раніше саме факт виклику cookies() у файлі змушував Next.js позначити усю сторінку як динамічну, навіть якщо cookies читались у крихітному компоненті на 100 символів.
Із асинхронним API все інакше. Промис-об'єкт cookies() можна створити навіть у статичному контексті: Next.js резолвить його лише тоді, коли ви робите await, і саме в цей момент рендер переключається у динамічний режим. Це лінива активація динаміки: усе до await може бути пре-рендерене, все після, тільки під час запиту. У парі з <Suspense> це дозволяє віддавати LCP-shell за 100 мс, поки cookies-залежна частина ще завантажується.
Друга причина, уніфікація моделі даних. У React 19 з'явився хук use(), який працює з будь-яким Promise. Тепер cookies, headers, params, searchParams та звичайні fetch-запити всі однакові за формою: Promise, який ви або await-ите у Server Component, або передаєте у use() у Client Component. Одна ментальна модель, менше багів.
Як мігрувати cookies() з Next.js 15 на 16
Розглянемо конкретний приклад: читання JWT з cookies у layout, щоб визначити авторизованого користувача. У Next.js 14/15 (синхронний код) це виглядало так:
// app/dashboard/layout.tsx (Next.js 15, більше не працює)
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { verifyJwt } from '@/lib/auth';
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const cookieStore = cookies(); // синхронно
const token = cookieStore.get('session')?.value;
if (!token) redirect('/login');
const user = verifyJwt(token);
return <div data-user={user.email}>{children}</div>;
}
У Next.js 16 той самий код перетворюється так. Компонент має стати async, а cookies() awaited:
// app/dashboard/layout.tsx (Next.js 16)
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { verifyJwt } from '@/lib/auth';
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies(); // await обов'язковий
const token = cookieStore.get('session')?.value;
if (!token) redirect('/login');
const user = verifyJwt(token);
return <div data-user={user.email}>{children}</div>;
}
Той самий патерн підходить для headers() і draftMode():
// app/api/geo/route.ts
import { headers } from 'next/headers';
export async function GET() {
const h = await headers();
const country = h.get('x-vercel-ip-country') ?? 'UA';
return Response.json({ country });
}
// app/preview/page.tsx
import { draftMode } from 'next/headers';
export default async function PreviewPage() {
const { isEnabled } = await draftMode();
return <p>Draft mode: {isEnabled ? 'ON' : 'OFF'}</p>;
}
Один нюанс. Якщо ви записуєте cookie у Server Action, set() і delete() викликаються на об'єкті, який ви отримали післяawait cookies(). Пишіть (await cookies()).set('name', 'value') або збережіть у змінну. Уникайте ланцюжка викликів без збереження, це роблять статичні аналізатори помилково недосяжним.
Асинхронні params та searchParams: нова сигнатура
Ось як має виглядати динамічна сторінка app/products/[id]/page.tsx у Next.js 16:
generateMetadata, opengraph-image, twitter-image, icon: усі приймають params як Promise. Codemod часто пропускає ці файли, бо шукає лише page.tsx/layout.tsx.
Route Handlers (route.ts) отримують params у другому аргументі: { params: Promise<{ id: string }> }. Порядок аргументів той самий, змінюється лише тип.
Паралельний await. Якщо вам потрібні і params, і searchParams, і cookies(), зробіть const [p, sp, c] = await Promise.all([params, searchParams, cookies()]). Це виграє один раунд-тріп на високонавантажених сторінках.
Client Components: React.use() замість await
У Client Components ви не можете робити функцію async. React 19 не підтримує async Client Components (це заплановано, але поки експериментально через use client). Замість цього використовуйте хук use() з React 19, який синхронно розпаковує Promise і, за необхідності, «підвішує» рендер до найближчого <Suspense>.
// app/products/[id]/product-tabs.tsx
'use client';
import { use } from 'react';
type Props = { params: Promise<{ id: string }> };
export default function ProductTabs({ params }: Props) {
const { id } = use(params); // use(), не await
return <div data-product={id}>/* ... */</div>;
}
Той самий підхід працює для searchParams, і (з невеликим застереженням) для cookies()/headers(). Останні можна викликати лише на сервері, тому передавайте розпаковане значення пропсом:
// app/dashboard/layout.tsx (Server Component)
import { cookies } from 'next/headers';
import Sidebar from './sidebar';
export default async function Layout({ children }: { children: React.ReactNode }) {
const theme = (await cookies()).get('theme')?.value ?? 'light';
return (
<>
<Sidebar theme={theme} /> {/* серіалізується як звичайний prop */}
{children}
</>
);
}
Codemod від Next.js: що він робить і що пропускає
Vercel постачає офіційний codemod, який автоматизує близько 80% рутинної роботи. Запустіть його у корені проекту:
# canary — найсвіжіша версія, покриває всі патерни Next.js 16
npx @next/codemod@canary next-async-request-api .
# або точкова міграція окремих директорій
npx @next/codemod@canary next-async-request-api app/dashboard
Що codemod робить добре:
Додає async до default export у page.tsx та layout.tsx, коли бачить params. або searchParams..
Замінює const c = cookies() на const c = await cookies() у server-контексті.
У Client Components (з директивою 'use client') обгортає прямий доступ у use() з react.
Оновлює типи params/searchParams на Promise<...>.
Що codemod пропускає (і додає TODO-коментар):
Кастомні хуки та утиліти, які приймають cookies/headers через параметр. Codemod не бачить контекст виклику.
Умовний доступ: if (foo) cookies().get(...). Трансформація неоднозначна.
Мій особистий workflow після codemod виглядає так:
Запустити rg 'params\.' app/ --type ts та переглянути кожен збіг.
Пошукати rg 'cookies\(\)\.' app/ lib/, щоб знайти ланцюжки без await.
Прогнати tsc --noEmit. З новими типами багато місць «покажуть себе» як TypeScript-помилки.
Перевірити e2e-сценарії, які торкаються auth та preview mode, бо саме там cookies і draftMode.
Типізація через npx next typegen
Ручне писання { params: Promise<{ id: string; slug: string }> } швидко втомлює і (головне) розсинхронізовується з реальною структурою маршрутів. Next.js 16 постачає утиліту next typegen, яка генерує глобальні типи на основі вашої файлової структури:
# одноразово
npx next typegen
# постійно (додати у package.json)
{
"scripts": {
"dev": "next dev --turbopack",
"typegen": "next typegen",
"typecheck": "next typegen && tsc --noEmit"
}
}
Після цього у вашому проекті стають доступні глобальні хелпери:
Плюси очевидні. Якщо ви перейменуєте сегмент [id] на [productId], TypeScript одразу підсвітить усі місця, де стара назва більше не існує. У великих кодових базах це економить години. Я особисто зловив три забуті посилання на старий сегмент саме таким способом на минулому проєкті.
Практичні патерни: auth, i18n, темізація
Auth-мідлваре у proxy.ts
У проксі-мідлварі (proxy.ts) API cookies походить з request.cookies, а не з next/headers, і він синхронний. Але якщо ви ще використовуєте headers() з next/headers у server actions, викликаних з мідлвари, там усе async. Детальніше про перехід читайте у нашому гіді про Server Actions проти Route Handlers.
// app/layout.tsx
import { cookies } from 'next/headers';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const theme = (await cookies()).get('theme')?.value ?? 'light';
return (
<html lang="uk" data-theme={theme}>
<body>{children}</body>
</html>
);
}
Оскільки читання cookies тепер асинхронне, Next.js вміє тримати цю частину layout поза кешем PPR. Статичний shell рендериться без теми, а <html data-theme> заповнюється на межі запиту без гідратаційної помилки.
Часті помилки та як їх діагностувати
1. «Route used params.id. params should be awaited»
Означає, що ви десь читаєте params.id без попереднього await params. Ловіть grep-ом rg 'params\.[a-zA-Z]' app/ і додайте await.
2. «cookies() was called outside a request scope»
Ви викликаєте cookies() у файлі, який імпортується у Client Component або на етапі білду (наприклад, у generateStaticParams). Винесіть виклик у Server Component або Route Handler.
3. React error #418 у Client Component
Виникає, якщо ви передаєте Promise у Client Component і намагаєтесь await-ити його всередині клієнтської функції. Використовуйте use() і обгорніть компонент у <Suspense>.
4. Type error TS2339: Property 'id' does not exist on type 'Promise<{ id: string }>'
Ви забули await params. Або типи ще не оновлені, тож запустіть npx next typegen.
5. Тест падає з «cookies is not a function»
Ваш mock повертає об'єкт напряму. Оберніть у Promise: vi.mock('next/headers', () => ({ cookies: () => Promise.resolve(new Map()) })).
Чи можна залишити синхронний доступ до cookies() у Next.js 16?
Ні. У Next.js 16 синхронний шлях видалено повністю: спроба звернутись до cookies().get(...) без await викидає runtime-помилку. У Next.js 15 такий доступ ще працював із попередженням, але у 16-й версії deprecation завершено.
Як мігрувати cookies() з Next.js 15 на 16?
Запустіть npx @next/codemod@canary next-async-request-api . у корені проекту, потім вручну перегляньте всі TODO-коментарі, які додав codemod, і зробіть grep rg 'cookies\(\)\.' app/. Це знайде ланцюжки без await, які автоматика могла пропустити.
Чому params тепер повертає Promise?
Асинхронний доступ дозволяє Next.js активувати динамічний рендер лише в момент await, а не одразу при виклику. Це напряму пов'язано з Partial Prerendering: статичний shell пре-рендериться без затримок, а динамічна частина завантажується асинхронно всередині Suspense.
Як використати await cookies() у Client Component?
Не можна: cookies() з next/headers працює тільки на сервері. Прочитайте значення у Server Component або Layout, передайте пропсом у Client Component, або створіть Server Action, який повертає потрібне значення, і викличте його через useActionState.
Що робить npx next typegen?
Генерує глобальні TypeScript-типи PageProps, LayoutProps, RouteContext на основі вашої файлової структури. Замість ручного опису { params: Promise<{ id: string }> } ви пишете PageProps<'/products/[id]'>, і типи оновлюються автоматично при перейменуванні сегментів.
Чи впливає асинхронний cookies() на продуктивність?
Ні, накладних витрат мало. Проміс резолвиться синхронно на першому await, оскільки Next.js уже має об'єкт запиту. Виграш навпаки більший: сторінки з ліниво-динамічними cookies тепер частково пре-рендеряться, що прискорює LCP.
У Next.js 16 файл middleware.ts офіційно перейменовано на proxy.ts. Розбираємо причини, codemod-міграцію, нові обмеження runtime та безпечні патерни автентифікації, які витримають CVE-2025-29927.