Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Отримання даних у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Отримання даних у Next.js** App Router відбувається через `async/await` безпосередньо в серверних компонентах. Без `getServerSideProps` або `getStaticProps`. ```tsx const data = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } }).then(res => res.json()) ``` **Головне:** `cache: 'no-store'` для персональних даних, `next: { revalidate: N }` для ISR, `Promise.all` щоб уникнути waterfall-запитів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Отримання даних у Next.js App Router** відбувається безпосередньо в серверних компонентах через `async/await`. Без `getServerSideProps`, без `getStaticProps`, без спеціальних функцій. ## Теорія ### TL;DR - Серверний компонент може бути `async`-функцією, і `await` у ньому просто працює - `fetch` у Next.js кешується за замовчуванням, але поведінку можна налаштувати для кожного запиту окремо - `cache: 'no-store'` = свіжі дані щоразу (SSR). `next: { revalidate: N }` = stale-while-revalidate (ISR) - Послідовні `await` утворюють waterfall; для незалежних запитів використовуй `Promise.all` - Client Components потребують `'use client'` + `useEffect` або SWR для інтерактивних сценаріїв ### Простий приклад ```tsx // app/dashboard/page.tsx export default async function Dashboard() { const data = await fetch('https://api.itlead.org/users', { next: { revalidate: 3600 } // Кешується, оновлюється раз на годину }).then(res => res.json()); return ( <ul> {data.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); } // Без useEffect. Без useState. Без клієнтського коду. ``` Компонент `async`, тому `await` працює на верхньому рівні. Next.js запускає його на сервері, кешує результат і відправляє HTML клієнту. ### Як параметри кешування відповідають режимам рендерингу До App Router ти вибирав режим рендерингу для всієї сторінки: `getStaticProps` для SSG, `getServerSideProps` для SSR, `getStaticProps` з `revalidate` для ISR. Тепер усе це перекладається в параметри `fetch` на рівні окремого запиту: ```tsx // SSG: кешується до ручного або автоматичного оновлення const res = await fetch('https://api.itlead.org/docs') // SSR: свіжі дані на кожен запит const res = await fetch('https://api.itlead.org/feed', { cache: 'no-store' }) // ISR: кешується, оновлюється у фоні після N секунд const res = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } }) // Оновлення за тегом: мітиш запит, потім викликаєш revalidateTag() const res = await fetch('https://api.itlead.org/problems', { next: { tags: ['problems'] } }) ``` Це важлива зміна. Одна сторінка може поєднувати статичні й динамічні дані, бо кожен `fetch` налаштовується незалежно. Більше не потрібно вибирати стратегію для всієї сторінки одразу. ### Паралельне отримання даних Послідовні `await` утворюють waterfall. Якщо `getUser` займає 200 мс, а `getStats` 300 мс, послідовне виконання коштує 500 мс. Паралельне - 300 мс. ```tsx // Погано: кожен чекає на попередній export default async function DashboardPage() { const user = await getUser() const stats = await getStats() // чекає на getUser const activity = await getActivity() // чекає на getStats } // Добре: всі три стартують одночасно export default async function DashboardPage() { const [user, stats, activity] = await Promise.all([ getUser(), getStats(), getActivity() ]) } ``` Waterfall-патерн у dashboard-сторінках, де кожне джерело даних незалежне, може сповільнювати рендеринг у 3-4 рази. Виглядає непомітно в коді і вилазить тільки під навантаженням. ### Preload-патерн з React.cache `Promise.all` працює, коли дані запитуються в одному місці. Але якщо один і той самий запит потрібен у різних частинах дерева компонентів, краще запустити його якомога раніше і гарантувати, що він виконається лише раз. ```tsx // lib/problems.ts import { cache } from 'react' export const getProblems = cache(async () => { const res = await fetch('https://api.itlead.org/problems') return res.json() }) export function preloadProblems() { void getProblems() // Стартує запит, але не блокує } ``` ```tsx // app/problems/page.tsx import { getProblems, preloadProblems } from '@/lib/problems' export default async function ProblemsPage() { preloadProblems() // Запускає fetch заздалегідь // ...інший рендеринг, потім: const problems = await getProblems() // Вже виконується або з кешу } ``` `React.cache` гарантує: скільки б місць не викликало `getProblems` за один рендер, реальний запит піде лише один раз. Це серверний аналог дедуплікації SWR, але без клієнтського бандлу. ### Streaming з Suspense Якщо одне джерело даних відповідає повільно, не варто блокувати всю сторінку. Обгортання async-компонента в `Suspense` дозволяє рендерити решту сторінки відразу. ```tsx // app/reports/page.tsx import { Suspense } from 'react' async function SlowChart() { const data = await fetch('https://api.itlead.org/analytics', { next: { tags: ['charts'] } }).then(r => r.json()) return <Chart data={data} /> } async function QuickHeader() { const stats = await db.stats.findFirst() return <header>Користувачів: {stats.count}</header> } export default function Reports() { return ( <> <QuickHeader /> <Suspense fallback={<div>Завантаження графіка...</div>}> <SlowChart /> </Suspense> </> ) } ``` `QuickHeader` рендериться і стримується відразу. `SlowChart` потрапляє до клієнта, коли його fetch завершується. Решта сторінки не чекає. ### Отримання даних на клієнті Для інтерактивних функцій - пошуку, фільтрації - серверне отримання не підходить. Дані залежать від введення користувача, якого немає під час рендерингу. ```tsx 'use client' import { useEffect, useState } from 'react' export function ProblemSearch() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) useEffect(() => { if (!query) return const controller = new AbortController() fetch(`/api/search?q=${query}`, { signal: controller.signal }) .then(res => res.json()) .then(setResults) return () => controller.abort() // Скасовуємо при зміні запиту }, [query]) return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} /> <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul> </div> ) } ``` Початкові дані завантажуй на сервері і передавай у Client Components через props. Клієнтський `fetch` - тільки для дій користувача. ### Коли що використовувати - Статичний контент (документація, блог): `fetch` з кешем за замовчуванням - Персональні дані (профіль, налаштування): `fetch` з `cache: 'no-store'` - Контент, що часто змінюється (лідерборд, ціни): `next: { revalidate: N }` - Контент з CMS: `next: { tags: ['content'] }` + `revalidateTag` у Server Action - Пошук, фільтрація, пагінація: Client Component з `useEffect` або SWR - Повільні дані поруч зі швидким UI: обгорнути в `Suspense` ### Типові помилки **Послідовні `await` для незалежних джерел.** Розглянули вище, але це найпоширеніша причина повільних dashboard-сторінок у App Router. Завжди перевіряй, чи один `await` справді залежить від попереднього. **Забути `cache: 'no-store'` для персональних даних.** ```tsx // Неправильно: один кешований відгук для всіх користувачів const user = await fetch(`/api/user/${id}`) // Правильно: свіжі дані для кожного запиту const user = await fetch(`/api/user/${id}`, { cache: 'no-store' }) ``` За замовчуванням кешований відгук може повернутися іншому користувачу. Для будь-яких персональних даних завжди відключай кеш. **`await fetch` у Client Component на верхньому рівні.** ```tsx // Неправильно: Client Components не можуть бути async 'use client' const data = await fetch('/api') // SyntaxError ``` Client Components - це звичайні функції, не `async`. Використовуй `useEffect` або SWR. **Мутація отриманих даних перед поверненням.** ```tsx // Неправильно: мутуємо спільний об'єкт у кеші const data = await fetch(url).then(r => r.json()) data.push(extraItem) // Змінює об'єкт для всіх наступних читань ``` Next.js кешує тіло відповіді. Мутація об'єкта змінює його для всіх наступних читань з цього кешу. Клонуй перед змінами: `structuredClone(await res.json())`. **Ревалідація за часом там, де потрібні миттєві оновлення.** Якщо контент змінюється непередбачувано (публікація в CMS, зміна ціни), `revalidate: 300` означає до 5 хвилин застарілих даних. Краще використовувати теги й `revalidateTag` у Server Action або Route Handler. ### Де зустрічається в реальних проектах - T3 Stack (Next.js + tRPC + Prisma): Server Components з `await db.query()` для адмін-панелей - Vercel Commerce: `fetch` Stripe API з `revalidate: 300` для product dashboard - Supabase: `createServerClient` у Server Components для автентифікації та даних користувача - Shadcn/UI docs: статичний `fetch` MDX-файлів під час збирання ### Питання на співбесіді **Q:** Яка різниця між `next.revalidate` і `cache: 'no-store'`? **A:** `revalidate` кешує відповідь і оновлює її у фоні після N секунд (stale-while-revalidate). `no-store` повністю пропускає кеш і щоразу отримує свіжі дані. **Q:** Що станеться, якщо `fetch` кине помилку в Server Component? **A:** Помилка піде до найближчого `error.tsx`. Додай error boundary на відповідному сегменті маршруту, щоб обробити її коректно. **Q:** Як `React.cache` відрізняється від вбудованої дедуплікації `fetch` у Next.js? **A:** Next.js автоматично дедуплює ідентичні `fetch`-виклики (однакові URL + опції) в межах одного запиту. `React.cache` працює з будь-якою async-функцією, не тільки з `fetch`, тому підходить для Prisma-запитів та інших джерел даних. **Q:** Коли брати SWR або TanStack Query замість `useEffect`? **A:** Коли потрібні автоматична ревалідація при фокусі, polling, optimistic updates або складне управління кешем. `useEffect` підходить для простих разових запитів; бібліотеки краще обробляють edge cases. **Q:** (Senior) Як Next.js дедуплює `fetch` через кордони `Suspense`? **A:** Ідентичні `fetch`-виклики в межах одного RSC-рендеру дедуплюються через in-memory кеш, прив'язаний до запиту. Навіть якщо два компоненти в `Suspense` викликають один URL одночасно, Next.js робить один реальний мережевий запит і ділить результат між ними. ## Приклади ### Базовий: Server Component з прямим запитом до бази даних ```tsx // app/problems/page.tsx import { db } from '@/lib/db' export default async function ProblemsPage() { const problems = await db.problem.findMany({ orderBy: { difficulty: 'asc' } }) return ( <ul> {problems.map(p => ( <li key={p.id}>{p.name} ({p.difficulty})</li> ))} </ul> ) } // Виконується на сервері. Prisma-клієнт не потрапляє в клієнтський бандл. ``` Пряме звернення до бази в компоненті. Без API-маршруту. Connection string і ORM залишаються на сервері. ### Середній: Dashboard з паралельними запитами і різним кешуванням ```tsx // app/admin/products/page.tsx import { db } from '@/lib/prisma' export default async function ProductsPage() { const [products, sales] = await Promise.all([ db.product.findMany({ include: { category: true } }), fetch('https://api.stripe.com/v1/analytics', { headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` }, next: { revalidate: 300 } // Дані Stripe оновлюються кожні 5 хв }).then(r => r.json()) ]) return ( <div> <h1>Продукти ({products.length})</h1> <pre>{JSON.stringify(sales.summary, null, 2)}</pre> </div> ) } // DB-запит і Stripe fetch виконуються паралельно. // Stripe кешується; DB завжди свіжа. ``` Два джерела даних, дві стратегії кешування, один паралельний запит. Prisma не проходить через `fetch`, тому Next.js не кешує його автоматично. Stripe-виклик - кешується. ### Просунутий: Streaming dashboard з Suspense ```tsx // app/reports/page.tsx import { Suspense } from 'react' import { db } from '@/lib/db' async function SalesChart() { const data = await fetch('https://api.itlead.org/analytics/heavy', { next: { tags: ['charts'] } }).then(r => r.json()) return <Chart data={data} /> } async function QuickStats() { const stats = await db.stats.findFirst() return <p>Користувачів: {stats.count}</p> } export default function ReportsPage() { return ( <main> <QuickStats /> <Suspense fallback={<p>Завантаження графіка...</p>}> <SalesChart /> </Suspense> </main> ) } // QuickStats рендериться і стримується відразу. // SalesChart стримується, коли повільний fetch завершується. // Сторінка не блокується. ``` `ReportsPage` сам не `async`. Він тільки компонує два async-компоненти. Кожен з них керує своїми даними самостійно, а Suspense перетворює повільний графік на неблокуючий слот.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.