Получение данных в React Server Components
Получайте данные в React Server Components через async-функции, разберитесь в кэше Next.js 15/16 и избегайте waterfall с Promise.all.
В React Server Component данные получают, делая компонент async-функцией и используя await непосредственно в теле функции — без useEffect, без состояния загрузки и без клиентского API round-trip. Это стандартная модель в Next.js App Router, которая меняет две привычки, унаследованные от клиентского React: получение данных перемещается в рендер, а главная ловушка — кэширование, которое изменило поведение в Next.js 15. В этой статье рассматривается актуальный подход, реальность кэширования в Next.js 16, почему "use server" не имеет отношения к Server Components, и как избежать последовательных водопадов запросов.
Ключевые выводы
- В Server Component данные получают, делая компонент
asyncи используяawaitдля запроса; поскольку Server Components рендерятся на сервере, учётные данные и логика запросов не попадают в клиентский бандл, поэтому можно напрямую обращаться к базе данных через ORM. - По умолчанию fetch-запросы не кэшируются в актуальных версиях Next.js — это противоположно поведению Next.js 13/14, которое по-прежнему описывается во многих устаревших руководствах.
- Для кэширования необходимо явно указать это:
{ cache: 'force-cache' }или{ next: { revalidate } }в предыдущей модели, либо директиваuse cacheпри условии, что установлен флагcacheComponents: true. - Директивы для Server Components не существует; директива “use server” используется для Server Functions (Server Actions) — это отдельная функциональность.
- Запускайте независимые запросы без
await, а затем разрешайте их черезPromise.all, чтобы избежать последовательного водопада.
Как получить данные в Server Component?
Основной подход — единственное изменение: сделать компонент async-функцией и использовать await для запроса. Server Components являются стандартными для layouts и pages в App Router, поэтому никакая директива не нужна — async-компоненты это возможность Server Components, которая позволяет использовать await в рендере.
// app/blog/page.tsx
export default async function Page() {
const res = await fetch('https://api.vercel.app/blog')
const posts = await res.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Поскольку код выполняется только на сервере, можно обойти API-слой и обращаться к источнику данных напрямую. Допускается безопасное выполнение запросов к базе данных через ORM или клиент базы данных, однако следует по-прежнему обеспечивать надлежащую аутентификацию и авторизацию запросов.
// app/blog/page.tsx
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Сравните это с клиентским подходом, который вы замещаете: получение данных в useEffect, сохранение результата в useState и отправка логики запроса (вместе с возможными секретами) в браузер. Такой подход создаёт дополнительный клиент-серверный round-trip после загрузки страницы; ожидание на сервере через await его устраняет.
Discover how at OpenReplay.com.
fetch не кэшируется по умолчанию — изменение в Next.js 15/16
В актуальных версиях Next.js fetch не кэшируется по умолчанию — это важно подчеркнуть, поскольку большинство устаревших материалов утверждают обратное. Согласно актуальной документации Next.js по fetch, значение по умолчанию — auto no cache: Next.js получает ресурс с удалённого сервера при каждом запросе. Предыдущая модель кэширования вела себя аналогично — по умолчанию fetch-запросы не кэшируются, и для кэширования отдельного запроса нужно установить параметр cache в значение 'force-cache'. В Next.js 13/14 обычный fetch кэшировался (force-cache) по умолчанию, поэтому любые руководства, опирающиеся на это поведение, устарели.
Для кэширования в рамках предыдущей модели укажите это явно для каждого запроса:
// Кэшировать до ручной ревалидации
await fetch('https://api.example.com/posts', { cache: 'force-cache' })
// Отдавать кэшированные данные, ревалидировать не чаще раза в 3600 секунд
await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } })
В Next.js 16 также появилась вторая модель — Cache Components, для подключения которой используется директива use cache. Распространённая ошибка: use cache — это функциональность Cache Components; чтобы её включить, добавьте параметр cacheComponents в файл next.config.ts. Без этого флага use cache не работает, и вы возвращаетесь к параметрам fetch, описанным выше.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true }
export default nextConfig
import { cacheLife } from 'next/cache'
export async function getPosts() {
'use cache'
cacheLife('hours')
const res = await fetch('https://api.example.com/posts')
return res.json()
}
Здесь директива use cache кэширует возвращаемое значение async-функций и компонентов, а cacheLife() задаёт продолжительность кэширования. Обратите внимание: в Next.js 16 unstable_cache заменяется директивой use cache — не используйте его в новом коде.
| Cache Components ВЫКЛ (предыдущая модель) | Cache Components ВКЛ (cacheComponents: true) | |
|---|---|---|
fetch по умолчанию | Без кэша | Без кэша |
| Кэширование fetch | { cache: 'force-cache' } | Директива use cache |
| Ревалидация по времени | { next: { revalidate: n } } | Профиль cacheLife() |
| Кэширование данных не из fetch | React.cache / конфигурация маршрута | use cache на функции |
Два поведения не зависят от кэширования. Во-первых, GET-запросы через fetch с одинаковыми URL и параметрами автоматически мемоизируются в рамках одного серверного рендер-прохода: если один и тот же запрос вызывается в нескольких компонентах, Next.js выполняет его один раз и передаёт результат всем. Во-вторых, для источников данных, не использующих fetch, оберните вызов в React.cache(); React.cache ограничен текущим запросом — каждый запрос получает собственную область мемоизации без совместного использования между запросами.
”use server” не создаёт Server Component
Если что-то и сбивает с толку новичков в RSC, то именно эта директива. Распространённое заблуждение — что Server Components обозначаются директивой “use server”, однако директивы для Server Components не существует; директива “use server” используется для Server Functions. Server Components — это просто стандарт в App Router: файл без директивы уже является Server Component. "use server" помечает Server Functions (Server Actions), и согласно документации React, Server Functions предназначены для мутаций, изменяющих серверное состояние; их не рекомендуется использовать для получения данных. Используйте обычные async Server Components для чтения данных, а "use server" — для их записи.
Избегайте водопадов и стримируйте медленные данные
Последовательное ожидание запросов через await создаёт водопад. Даже внутри одного компонента несколько async/await-запросов могут выполняться последовательно, если они расположены один за другим; запускайте несколько запросов через вызов fetch, а затем ожидайте их через Promise.all. Вызов функций без await запускает их немедленно:
export default async function Page({ params }: { params: Promise<{ username: string }> }) {
const { username } = await params
const artistData = getArtist(username) // запускается сразу
const albumsData = getAlbums(username) // запускается сразу
const [artist, albums] = await Promise.all([artistData, albumsData])
return <h1>{artist.name}</h1>
}
Для действительно медленных данных не блокируйте маршрут. Если запросы данных выполняются медленно, весь маршрут блокируется до завершения всех запросов; чтобы улучшить время загрузки, разбейте страницу на части и постепенно отправляйте их клиенту. Оберните медленное поддерево в <Suspense> (или добавьте loading.js), а для данных с низким приоритетом запустите промис на сервере и читайте его на клиенте через API use:
// Server Component — обратите внимание: commentsPromise НЕ ожидается через await
import { Suspense } from 'react'
import Comments from './comments'
export default async function Page({ id }: { id: string }) {
const commentsPromise = getComments(id)
return (
<Suspense fallback={<p>Загрузка комментариев…</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}
// Client Component
'use client'
import { use } from 'react'
export default function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return comments.map((c) => <p key={c.id}>{c.text}</p>)
}
Вы запускаете промис на сервере и ожидаете его на клиенте через API use; поскольку async-компоненты не поддерживаются на клиенте, промис ожидается через use. Когда что-то идёт не так, ошибка видна только там, где находится пользователь — в виде Suspense-заглушки, которая так и не разрешается в контент, или стримируемого фрагмента, который приходит после гидратации — и именно запись сессии позволяет восстановить точную картину того, что браузер отрендерил в этот момент.
Храните секреты на серверной стороне границы
Данные, полученные на сервере, передаются в Client Components через props, и эти props должны быть сериализуемыми — передавайте простые данные, а не функции или экземпляры классов. Секреты остаются на месте по умолчанию, поскольку код Server Component никогда не отправляется в браузер, однако общие модули могут привести к утечке. В Next.js в клиентский бандл включаются только переменные окружения с префиксом NEXT_PUBLIC_; переменные без этого префикса Next.js заменяет пустой строкой. Чтобы защитить модуль, содержащий секреты, импортируйте пакет server-only в его начале — тогда случайный клиентский импорт завершится ошибкой на этапе сборки. React Context также не может находиться в Server Component — оберните его в провайдер с 'use client' и рендерите этот провайдер внутри серверного layout.
Смена мышления небольшая, но полная: переместите получение данных в async-компонент, используйте await и воспринимайте кэширование как то, во что вы явно включаетесь, а не как то, что происходит само по себе. Начните с переноса одного useEffect-запроса данных в async Server Component, убедитесь, что запрос выполняется на стороне сервера, а затем решайте для каждого маршрута, нужно ли кэшировать эти данные с помощью { next: { revalidate } } или директивы use cache.
Часто задаваемые вопросы
Кэшируется ли fetch по умолчанию в Next.js 16?
Нет. Начиная с Next.js 15 и в версии 16, fetch не кэшируется по умолчанию и обращается к источнику при каждом запросе. Это противоположно поведению Next.js 13/14, где обычный fetch кэшировался с force-cache по умолчанию. В предыдущей модели кэширование включается явно для каждого запроса: параметром cache со значением force-cache или значением next revalidate; устаревшие руководства, утверждающие обратное, не актуальны.
В чём разница между use cache и next revalidate для кэширования fetch?
Они относятся к двум разным моделям. Параметр next revalidate и force-cache работают в предыдущей, стандартной модели кэширования без дополнительной конфигурации. Директива use cache — это функциональность Cache Components, которая вступает в силу только при установке cacheComponents в значение true в next.config.ts; продолжительность кэширования задаётся через cacheLife. Без этого флага use cache не работает, и fetch, который вы намеревались кэшировать, будет молча обращаться к источнику при каждом запросе.
Можно ли использовать useEffect для получения данных в Server Component?
Нет. useEffect выполняется только в браузере, поэтому он не может работать внутри Server Component, который рендерится исключительно на сервере. В Server Component компонент делается async, а запрос ожидается непосредственно в рендере — без состояния загрузки и без клиентского round-trip. Если необходимо получать данные на клиенте, добавьте директиву use client, чтобы сделать компонент клиентским, либо запустите промис на сервере и читайте его на клиенте через API use из React.
Превращает ли директива use server компонент в Server Component?
Нет. Директивы для Server Components не существует; они являются стандартом в App Router, поэтому файл без директивы уже является Server Component. Директива use server помечает Server Functions, также называемые Server Actions, которые предназначены для мутаций, изменяющих серверное состояние, и не рекомендуются для получения данных. Используйте обычный async Server Component для чтения данных и use server — для их записи.
Что происходит, если один из запросов завершается ошибкой при использовании Promise.all для параллельных запросов?
Promise.all отклоняется, как только любой из запросов завершается с ошибкой, отбрасывая результаты остальных и приводя к сбою всего рендера. Если один запрос должен иметь право завершиться с ошибкой, не затрагивая остальные, используйте Promise.allSettled — он разрешается со статусом каждого запроса и позволяет обрабатывать ошибки по отдельности. Запускайте запросы без await для параллельного выполнения, а затем выбирайте между Promise.all и Promise.allSettled в зависимости от того, как вы хотите обрабатывать ошибки.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k