Skip to main content

Отримання даних у Next.js

Отримання даних у 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 перетворює повільний графік на неблокуючий слот.

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

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

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

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