Data Fetching в Next.js
В App Router загрузка данных происходит напрямую в серверных компонентах. Не нужны специальные функции вроде getServerSideProps или getStaticProps. Компонент просто делает await на нужный запрос.
Загрузка данных в серверных компонентах
// 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}</li>
))}
</ul>
)
}
Серверный компонент может напрямую обращаться к базе данных, файловой системе или внешним API. Код и зависимости не попадают в клиентский бандл.
fetch с кешированием
Next.js расширяет стандартный fetch, добавляя кеширование и ревалидацию:
// Кешируется по умолчанию (эквивалент SSG)
const res = await fetch('https://api.hackfrontend.com/docs')
// Не кешировать (эквивалент SSR)
const res = await fetch('https://api.hackfrontend.com/feed', {
cache: 'no-store'
})
// Ревалидация по времени (эквивалент ISR)
const res = await fetch('https://api.hackfrontend.com/problems', {
next: { revalidate: 300 }
})
// Ревалидация по тегу
const res = await fetch('https://api.hackfrontend.com/problems', {
next: { tags: ['problems'] }
})
Параллельная загрузка данных
Частая ошибка: последовательные запросы, которые можно выполнить параллельно.
// Плохо: последовательные запросы
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()
])
// ...
}
Паттерн: предзагрузка данных
Для параллельной загрузки на уровне компонентного дерева используется паттерн preload:
// lib/problems.ts
import { cache } from 'react'
export const getProblems = cache(async () => {
const res = await fetch('https://api.hackfrontend.com/problems')
return res.json()
})
export function preloadProblems() {
void getProblems()
}
// app/problems/page.tsx
import { getProblems, preloadProblems } from '@/lib/problems'
import { ProblemList } from './problem-list'
export default async function ProblemsPage() {
preloadProblems()
return <ProblemList />
}
React.cache гарантирует, что запрос выполнится только один раз, даже если getProblems вызывается в нескольких местах.
Загрузка данных на клиенте
Для интерактивных сценариев (поиск, фильтрация) данные загружаются на клиенте:
'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>
)
}
Практический совет:
Загружай начальные данные на сервере и передавай клиентским компонентам через props. Клиентскую загрузку используй только для данных, которые зависят от действий пользователя.
Полезные ресурсы
- nextjs.org/docs — Data Fetching
- Data Fetching Patterns