Паттерны и лучшие практики

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

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

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

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

Для изменения или обновления данных можно использовать Server Actions.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Например, компонент Playlists начнёт получать данные только после завершения запроса в компоненте Artist, так как зависит от пропса 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> (для вложенных компонентов), чтобы показать состояние загрузки во время потоковой передачи результата.

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

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

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

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

Защита конфиденциальных данных от утечки на клиент

Рекомендуется использовать API React для пометки данных taintObjectReference и taintUniqueValue, чтобы предотвратить передачу целых объектов или конфиденциальных значений на клиент.

Для включения пометки в приложении установите опцию experimental.taint в конфигурации Next.js в значение true:

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

Затем передайте объект или значение, которые нужно пометить, в функции experimental_taintObjectReference или experimental_taintUniqueValue:

import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Не передавайте весь объект пользователя на клиент',
    data
  )
  experimental_taintUniqueValue(
    "Не передавайте адрес пользователя на клиент",
    data,
    data.address
  )
  return data
}
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Не передавайте весь объект пользователя на клиент',
    data
  )
  experimental_taintUniqueValue(
    "Не передавайте адрес пользователя на клиент",
    data,
    data.address
  )
  return data
}
import { getUserData } from './data'

export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // вызовет ошибку из-за taintObjectReference
      address={userData.address} // вызовет ошибку из-за taintUniqueValue
    />
  )
}
import { getUserData } from './data'

export async function Page() {
  const userData = await getUserData()
  return (
    <ClientComponent
      user={userData} // вызовет ошибку из-за taintObjectReference
      address={userData.address} // вызовет ошибку из-за taintUniqueValue
    />
  )
}

Подробнее о безопасности и Server Actions.