Kodokon kodokon.com

Современный React: Server Components, use, useTransition, useOptimistic

Изучи серверные компоненты, чтение промисов через use и оптимистичные обновления React 19.

11 мин · 3 вопросов

Открыть этот урок в Kodokon

React Server Components (RSC) заново проводят границу между клиентом и сервером. Серверный компонент выполняется только на сервере: его код никогда не попадает в клиентский бандл, он может быть async, напрямую читать базу данных или файловую систему, а его результат отправляется в браузер как сериализованный поток (RSC payload), который React объединяет с клиентским деревом. Взамен у него нет состояния, нет эффектов и нет доступа к браузеру: никакого useState, никакого useEffect, никаких обработчиков событий. На клиенте он никогда не рендерится повторно.

JSX
// Server component: never sent to the client.
export default async function NotesPage() {
  const notes = await db.notes.findAll();
  return (
    <ul>
      {notes.map((note) => (
        <li key={note.id}>{note.title}</li>
      ))}
    </ul>
  );
}
Асинхронный серверный компонент напрямую обращается к базе данных.

Интерактивность живёт в клиентских компонентах, помеченных директивой 'use client' в начале файла. Эта директива помечает не "клиентский компонент", а границу: всё, что импортирует этот модуль, тоже переезжает на клиентскую сторону. Пропы, пересекающие границу, должны быть сериализуемыми: простые объекты, массивы, промисы... но никогда обычные функции или экземпляры классов.

JSX
'use client';

import { useState } from 'react';

export function LikeButton({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return (
    <button onClick={() => setCount(count + 1)}>
      {count} likes
    </button>
  );
}
Директива 'use client' очерчивает интерактивную зону.

React 19 вводит use - API, который читает значение промиса или контекста. В отличие от хуков, use можно вызывать внутри условия или цикла. Получив промис, он приостанавливает компонент до разрешения, опираясь на ближайшую границу Suspense. Идиоматичный паттерн: серверный компонент создаёт промис, не дожидаясь его, передаёт его пропом, а клиентский компонент потребляет его через use: сервер начинает стримить страницу, не дожидаясь данных.

JSX
import { use, Suspense } from 'react';

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise);
  return comments.map((c) => (
    <p key={c.id}>{c.text}</p>
  ));
}

function Page({ commentsPromise }) {
  return (
    <Suspense fallback={<Spinner />}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}
use приостанавливает компонент до разрешения промиса.

Со стороны взаимодействия useTransition помечает обновление как несрочное и выставляет isPending, чтобы показать сдержанный индикатор, не блокируя ввод. useOptimistic дополняет этот дуэт для сетевых мутаций: ты сразу показываешь ожидаемое состояние, пока выполняется асинхронное действие; когда действие завершается (успешно или нет), React отбрасывает оптимистичное значение и снова показывает настоящее состояние, то, что идёт от источника истины.

JSX
const [optimisticLikes, addOptimisticLike] =
  useOptimistic(likes, (count, delta) => count + delta);

function handleLike() {
  startTransition(async () => {
    addOptimisticLike(1);
    await saveLike();
    // At the end, React shows the real state again.
  });
}
Интерфейс реагирует мгновенно, сервер подтверждает потом.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Чего не может серверный компонент?
    • Быть объявленным async и ожидать запрос
    • Напрямую читать базу данных
    • Использовать useState и обработчики событий
    • Рендерить клиентские компоненты как потомков
  2. Какое правило хуков позволено нарушать API use?
    • Вызываться вне компонента
    • Вызываться внутри условия или цикла
    • Вызываться после раннего возврата из модуля
    • Вызываться внутри класса
  3. Что происходит со значением useOptimistic, когда асинхронное действие завершается?
    • Оно становится постоянным состоянием компонента
    • Оно отбрасывается, и снова вступает в силу настоящее состояние
    • Оно объединяется с настоящим состоянием
    • Оно сохраняется до следующего ручного рендера