Как получать данные и использовать потоковую передачу

Эта страница расскажет, как получать данные в Серверных и Клиентских компонентах и как потоково передавать компоненты, зависящие от данных.

Получение данных

Серверные компоненты

Вы можете получать данные в Серверных компонентах с помощью:

  1. API fetch
  2. ORM или базы данных

С помощью 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>
  )
}

Полезно знать:

С помощью 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>
  )
}

Клиентские компоненты

Есть два способа получать данные в Клиентских компонентах:

  1. С помощью хука use от React
  2. С помощью сторонних библиотек, таких как 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 страницы на меньшие части и постепенно отправлять их с сервера на клиент.

Как работает серверный рендеринг с потоковой передачей

Есть два способа реализовать потоковую передачу в вашем приложении:

  1. Обернуть страницу файлом loading.js
  2. Обернуть компонент <Suspense>

С помощью loading.js

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

Структура папки блога с файлом loading.js
export default function Loading() {
  // Определите UI загрузки здесь
  return <div>Loading...</div>
}
export default function Loading() {
  // Определите UI загрузки здесь
  return <div>Loading...</div>
}

При навигации пользователь сразу увидит макет и состояние загрузки, пока страница рендерится. Новый контент автоматически заменится, как только рендеринг завершится.

UI загрузки

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

Обзор loading.js

Этот подход хорошо работает для сегментов маршрутов (макетов и страниц), но для более детальной потоковой передачи вы можете использовать <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) => {
  // ...
})