Паттерны получения данных

Существует несколько рекомендуемых паттернов и лучших практик для получения данных в React и Next.js. На этой странице рассматриваются наиболее распространённые из них и способы их применения.

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

По возможности мы рекомендуем получать данные на сервере. Это позволяет:

  • Иметь прямой доступ к серверным ресурсам данных (например, базам данных).
  • Повысить безопасность приложения, предотвращая утечку конфиденциальной информации, такой как токены доступа и API-ключи, на клиент.
  • Получать данные и выполнять рендеринг в одной среде. Это сокращает обмен данными между клиентом и сервером, а также нагрузку на основной поток на клиенте.
  • Выполнять несколько запросов данных за один обход вместо множества отдельных запросов на клиенте.
  • Уменьшать количество водопадов между клиентом и сервером.
  • В зависимости от региона, получение данных может происходить ближе к источнику данных, что снижает задержки и повышает производительность.

Вы можете получать данные на сервере с помощью Серверных Компонентов (Server Components), Обработчиков Маршрутов (Route Handlers) и Серверных Действий (Server Actions).

Получение данных там, где они нужны

Если вам нужны одни и те же данные (например, текущий пользователь) в нескольких компонентах дерева, вам не нужно получать данные глобально или передавать пропсы между компонентами. Вместо этого вы можете использовать fetch или React cache в компоненте, которому нужны данные, не беспокоясь о влиянии на производительность из-за множественных запросов одних и тех же данных.

Это возможно, потому что запросы fetch автоматически мемоизируются. Подробнее о мемоизации запросов

Полезно знать: Это также применимо к макетам (layouts), так как передача данных между родительским макетом и его дочерними элементами невозможна.

Потоковая передача (Streaming)

Потоковая передача и Suspense — это возможности React, которые позволяют постепенно рендерить и инкрементально передавать отрендеренные части интерфейса на клиент.

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

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

Подробнее о потоковой передаче и Suspense читайте на страницах Интерфейс загрузки и Потоковая передача с Suspense.

Параллельное и последовательное получение данных

При получении данных внутри React-компонентов важно учитывать два паттерна: параллельный и последовательный.

Последовательное и параллельное получение данных
  • При последовательном получении данных запросы в маршруте зависят друг от друга, что создаёт водопады. Иногда этот паттерн полезен, когда один запрос зависит от результата другого или нужно дождаться выполнения условия перед следующим запросом для экономии ресурсов. Однако такое поведение может быть непреднамеренным и приводить к увеличению времени загрузки.
  • При параллельном получении данных запросы в маршруте инициируются одновременно, что сокращает водопады между клиентом и сервером и общее время загрузки данных.

Последовательное получение данных

Если у вас есть вложенные компоненты, и каждый компонент получает свои данные, то запросы данных будут выполняться последовательно, если эти запросы разные (это не относится к запросам одних и тех же данных, так как они автоматически мемоизируются).

Например, компонент Playlists начнёт получать данные только после того, как компонент Artist завершит их получение, потому что Playlists зависит от пропса artistID:

// ...

async function Playlists({ artistID }: { artistID: string }) {
  // Ожидаем плейлисты
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Ожидаем исполнителя
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
// ...

async function Playlists({ artistID }) {
  // Ожидаем плейлисты
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({ params: { username } }) {
  // Ожидаем исполнителя
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

В таких случаях можно использовать loading.js (для сегментов маршрута) или React <Suspense> (для вложенных компонентов), чтобы показать состояние загрузки, пока React передаёт результат.

Это предотвратит блокировку всего маршрута из-за получения данных, и пользователь сможет взаимодействовать с незаблокированными частями страницы.

Блокирующие запросы данных:

Альтернативный подход для предотвращения водопадов — получение данных глобально, в корне приложения, но это заблокирует рендеринг всех сегментов маршрута ниже, пока данные не будут загружены. Это можно описать как получение данных по принципу "всё или ничего". Либо у вас есть все данные для страницы или приложения, либо ничего.

Любые запросы с await будут блокировать рендеринг и получение данных для всего дерева ниже, если они не обёрнуты в границу <Suspense> или не используется loading.js. Другой вариант — использовать параллельное получение данных или паттерн предзагрузки.

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

Для параллельного получения данных можно заранее инициировать запросы, определив их вне компонентов, которые используют данные, а затем вызвать их внутри компонента. Это экономит время, запуская оба запроса параллельно, но пользователь не увидит результат рендеринга, пока не будут выполнены оба промиса.

В примере ниже функции getArtist и getArtistAlbums определены вне компонента Page, вызываются внутри компонента, и мы ожидаем выполнения обоих промисов:

import Albums from './albums'

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getArtistAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Инициируем оба запроса параллельно
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Ожидаем выполнения промисов
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}
import Albums from './albums'

async function getArtist(username) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getArtistAlbums(username) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({ params: { username } }) {
  // Инициируем оба запроса параллельно
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Ожидаем выполнения промисов
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

Для улучшения пользовательского опыта можно добавить Границу Suspense, чтобы разделить работу по рендерингу и как можно быстрее показать часть результата.

Предзагрузка данных

Ещё один способ предотвратить водопады — использовать паттерн предзагрузки. Вы можете создать функцию preload для дальнейшей оптимизации параллельного получения данных. При таком подходе не нужно передавать промисы через пропсы. Функция preload может иметь любое имя, так как это паттерн, а не API.

import { getItem } from '@/utils/get-item'

export const preload = (id: string) => {
  // void вычисляет выражение и возвращает undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
import { getItem } from '@/utils/get-item'

export const preload = (id) => {
  // void вычисляет выражение и возвращает undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }) {
  const result = await getItem(id)
  // ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // начинаем загрузку данных элемента
  preload(id)
  // выполняем другую асинхронную задачу
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({ params: { id } }) {
  // начинаем загрузку данных элемента
  preload(id)
  // выполняем другую асинхронную задачу
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}

Использование React cache, server-only и паттерна предзагрузки

Вы можете объединить функцию cache, паттерн предзагрузки и пакет server-only, чтобы создать утилиту для получения данных, которую можно использовать во всём приложении.

import { cache } from 'react'
import 'server-only'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})
import { cache } from 'react'
import 'server-only'

export const preload = (id) => {
  void getItem(id)
}

export const getItem = cache(async (id) => {
  // ...
})

При таком подходе вы можете заранее получать данные, кэшировать ответы и гарантировать, что получение данных происходит только на сервере.

Экспорты из utils/get-item могут использоваться макетами, страницами или другими компонентами, чтобы контролировать время получения данных элемента.

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

  • Рекомендуем использовать пакет server-only, чтобы гарантировать, что серверные функции получения данных никогда не используются на клиенте.