Как получать данные и использовать потоковую передачу
Эта страница расскажет, как получать данные в Серверных и Клиентских компонентах и как потоково передавать компоненты, зависящие от данных.
Получение данных
Серверные компоненты
Вы можете получать данные в Серверных компонентах с помощью:
С помощью API fetch
Чтобы получить данные с помощью API fetch
, превратите ваш компонент в асинхронную функцию и ожидайте вызов fetch
. Например:
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Полезно знать:
- Ответы
fetch
по умолчанию не кэшируются. Однако Next.js будет предварительно рендерить маршрут, и результат будет кэшироваться для улучшения производительности. Если вы хотите использовать динамический рендеринг, используйте опцию{ cache: 'no-store' }
. См. Справочник APIfetch
.- В процессе разработки вы можете логировать вызовы
fetch
для лучшей видимости и отладки. См. Справочник APIlogging
.
С помощью ORM или базы данных
Поскольку Серверные компоненты рендерятся на сервере, вы можете безопасно выполнять запросы к базе данных с помощью ORM или клиента базы данных. Превратите ваш компонент в асинхронную функцию и ожидайте вызов:
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>
)
}
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>
)
}
Клиентские компоненты
Есть два способа получать данные в Клиентских компонентах:
- С помощью хука
use
от React - С помощью сторонних библиотек, таких как SWR или React Query
Потоковая передача данных с помощью хука use
Вы можете использовать хук use
от React для потоковой передачи данных с сервера на клиент. Начните с получения данных в вашем Серверном компоненте и передайте промис в Клиентский компонент как пропс:
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// Не ожидайте функцию получения данных
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// Не ожидайте функцию получения данных
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
Затем в вашем Клиентском компоненте используйте хук use
для чтения промиса:
'use client'
import { use } from 'react'
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>
}) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
import { use } from 'react'
export default function Posts({ posts }) {
const posts = use(posts)
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
В примере выше компонент <Posts>
обёрнут в границу <Suspense>
. Это означает, что запасной вариант будет показан, пока промис не разрешится. Узнайте больше о потоковой передаче.
Сторонние библиотеки
Вы можете использовать сторонние библиотеки, такие как SWR или React Query, для получения данных в Клиентских компонентах. Эти библиотеки имеют свою собственную семантику для кэширования, потоковой передачи и других функций. Например, с SWR:
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Дедупликация запросов с помощью React.cache
Дедупликация — это процесс предотвращения дублирующих запросов к одному и тому же ресурсу во время рендеринга. Она позволяет получать одни и те же данные в разных компонентах, предотвращая множественные сетевые запросы к источнику данных.
Если вы используете fetch
, запросы можно дедуплицировать, добавив cache: 'force-cache'
. Это означает, что вы можете безопасно вызывать один и тот же URL с одинаковыми параметрами, и будет выполнен только один запрос.
Если вы не используете fetch
, а вместо этого работаете напрямую с ORM или базой данных, вы можете обернуть ваш запрос данных функцией React cache
.
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
export const getPost = cache(async (id: string) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
import { notFound } from 'next/navigation'
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
Потоковая передача
Предупреждение: Следующий контент предполагает, что в вашем приложении включена опция конфигурации
dynamicIO
. Этот флаг был введён в Next.js 15 canary.
При использовании async/await
в Серверных компонентах Next.js будет использовать динамический рендеринг. Это означает, что данные будут получаться и рендериться на сервере для каждого пользовательского запроса. Если есть медленные запросы данных, весь маршрут будет заблокирован для рендеринга.
Чтобы улучшить время начальной загрузки и пользовательский опыт, вы можете использовать потоковую передачу, чтобы разбить HTML страницы на меньшие части и постепенно отправлять их с сервера на клиент.

Есть два способа реализовать потоковую передачу в вашем приложении:
- Обернуть страницу файлом
loading.js
- Обернуть компонент
<Suspense>
С помощью loading.js
Вы можете создать файл loading.js
в той же папке, что и ваша страница, чтобы потоково передавать всю страницу во время получения данных. Например, чтобы потоково передавать app/blog/page.js
, добавьте файл в папку app/blog
.

export default function Loading() {
// Определите UI загрузки здесь
return <div>Loading...</div>
}
export default function Loading() {
// Определите UI загрузки здесь
return <div>Loading...</div>
}
При навигации пользователь сразу увидит макет и состояние загрузки, пока страница рендерится. Новый контент автоматически заменится, как только рендеринг завершится.

Внутри loading.js
будет вложен в layout.js
и автоматически обернёт файл page.js
и все дочерние элементы в границу <Suspense>
.

Этот подход хорошо работает для сегментов маршрутов (макетов и страниц), но для более детальной потоковой передачи вы можете использовать <Suspense>
.
С помощью <Suspense>
<Suspense>
позволяет более детально управлять тем, какие части страницы потоково передавать. Например, вы можете сразу показать любой контент страницы, который находится вне границы <Suspense>
, и потоково передать список постов блога внутри границы.
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* Этот контент будет отправлен клиенту сразу */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* Любой контент, обёрнутый в границу <Suspense>, будет потоково передаваться */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* Этот контент будет отправлен клиенту сразу */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* Любой контент, обёрнутый в границу <Suspense>, будет потоково передаваться */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
Создание осмысленных состояний загрузки
Мгновенное состояние загрузки — это запасной UI, который сразу показывается пользователю после навигации. Для лучшего пользовательского опыта мы рекомендуем проектировать состояния загрузки, которые помогают пользователям понять, что приложение реагирует. Например, вы можете использовать скелетоны и спиннеры или небольшую, но значимую часть будущих экранов, такую как обложка, заголовок и т. д.
В процессе разработки вы можете предварительно просматривать и проверять состояния загрузки ваших компонентов с помощью React Devtools.
Примеры
Последовательное получение данных
Последовательное получение данных происходит, когда вложенные компоненты в дереве получают свои собственные данные, и запросы не дедуплицируются, что приводит к увеличению времени ответа.

Могут быть случаи, когда вам нужен этот паттерн, потому что один запрос зависит от результата другого.
Например, компонент <Playlists>
начнёт получать данные только после того, как компонент <Artist>
завершит получение данных, потому что <Playlists>
зависит от пропса artistID
:
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// Получить информацию об артисте
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* Показать запасной UI, пока компонент Playlists загружается */}
<Suspense fallback={<div>Loading...</div>}>
{/* Передать ID артиста в компонент Playlists */}
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }: { artistID: string }) {
// Использовать ID артиста для получения плейлистов
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({ params }) {
const { username } = await params
// Получить информацию об артисте
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* Показать запасной UI, пока компонент Playlists загружается */}
<Suspense fallback={<div>Loading...</div>}>
{/* Передать ID артиста в компонент Playlists */}
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }) {
// Использовать ID артиста для получения плейлистов
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
Чтобы улучшить пользовательский опыт, вы должны использовать React <Suspense>
, чтобы показать fallback
во время получения данных. Это включит потоковую передачу и предотвратит блокировку всего маршрута последовательными запросами данных.
Параллельный сбор данных (Parallel data fetching)
Параллельный сбор данных происходит, когда запросы данных в маршруте инициируются одновременно и начинают выполняться в одно и то же время.
По умолчанию макеты и страницы рендерятся параллельно. Это означает, что каждый сегмент начинает собирать данные как можно раньше.
Однако внутри любого компонента несколько async
/await
запросов могут выполняться последовательно, если они расположены друг за другом. Например, getAlbums
будет заблокирован до завершения getArtist
:
import { getArtist, getAlbums } from '@/app/lib/data'
export default async function Page({ params }) {
// Эти запросы будут выполняться последовательно
const { username } = await params
const artist = await getArtist(username)
const albums = await getAlbums(username)
return <div>{artist.name}</div>
}
Вы можете инициировать запросы параллельно, определяя их вне компонентов, которые используют данные, и разрешая их вместе, например, с помощью Promise.all
:
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
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>
<Albums list={albums} />
</>
)
}
import Albums from './albums'
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({ params }) {
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>
<Albums list={albums} />
</>
)
}
Полезно знать: Если один запрос завершится ошибкой при использовании
Promise.all
, вся операция завершится неудачей. Чтобы обработать это, можно использовать методPromise.allSettled
.
Предзагрузка данных (Preloading data)
Вы можете предзагружать данные, создав вспомогательную функцию, которую вызываете заранее перед блокирующими запросами. <Item>
условно рендерится на основе функции checkIsAvailable()
.
Вы можете вызвать preload()
до checkIsAvailable()
, чтобы заранее инициировать зависимости данных <Item/>
. К моменту рендеринга <Item/>
его данные уже будут загружены.
import { getItem } from '@/lib/data'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// начинаем загрузку данных элемента
preload(id)
// выполняем другую асинхронную задачу
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
export const preload = (id: string) => {
// void вычисляет выражение и возвращает undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
import { getItem } from '@/lib/data'
export default async function Page({ params }) {
const { id } = await params
// начинаем загрузку данных элемента
preload(id)
// выполняем другую асинхронную задачу
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
export const preload = (id) => {
// void вычисляет выражение и возвращает undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }) {
const result = await getItem(id)
// ...
Дополнительно можно использовать функцию cache
из React и пакет server-only
для создания переиспользуемой вспомогательной функции. Этот подход позволяет кэшировать функцию сбора данных и гарантировать, что она выполняется только на сервере.
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id) => {
void getItem(id)
}
export const getItem = cache(async (id) => {
// ...
})