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. Клиентскую загрузку используй только для данных, которые зависят от действий пользователя.

Полезные ресурсы

Связанные темы