Отримання даних у 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 для інтерактивних сценаріїв
Простий приклад
// 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 на рівні окремого запиту:
// 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 мс.
// Погано: кожен чекає на попередній
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 працює, коли дані запитуються в одному місці. Але якщо один і той самий запит потрібен у різних частинах дерева компонентів, краще запустити його якомога раніше і гарантувати, що він виконається лише раз.
// 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() // Стартує запит, але не блокує
}// 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 дозволяє рендерити решту сторінки відразу.
// 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 завершується. Решта сторінки не чекає.
Отримання даних на клієнті
Для інтерактивних функцій - пошуку, фільтрації - серверне отримання не підходить. Дані залежать від введення користувача, якого немає під час рендерингу.
'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' для персональних даних.
// Неправильно: один кешований відгук для всіх користувачів
const user = await fetch(`/api/user/${id}`)
// Правильно: свіжі дані для кожного запиту
const user = await fetch(`/api/user/${id}`, { cache: 'no-store' })За замовчуванням кешований відгук може повернутися іншому користувачу. Для будь-яких персональних даних завжди відключай кеш.
await fetch у Client Component на верхньому рівні.
// Неправильно: Client Components не можуть бути async
'use client'
const data = await fetch('/api') // SyntaxErrorClient Components - це звичайні функції, не async. Використовуй useEffect або SWR.
Мутація отриманих даних перед поверненням.
// Неправильно: мутуємо спільний об'єкт у кеші
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:
fetchStripe API зrevalidate: 300для product dashboard - Supabase:
createServerClientу Server Components для автентифікації та даних користувача - Shadcn/UI docs: статичний
fetchMDX-файлів під час збирання
Питання на співбесіді
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 з прямим запитом до бази даних
// 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 з паралельними запитами і різним кешуванням
// 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
// 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 перетворює повільний графік на неблокуючий слот.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.