lenec ru

← все посты

React 19 use() хук: чем заменяет useEffect

13K

Когда я первый раз увидел use() в RFC, реакция была: «зачем ещё один хук, если у нас уже есть useEffect и Suspense?». Через полгода работы с React 19 ответ такой: use() не «ещё один хук», а способ окончательно избавиться от useEffect в типичных сценариях получения данных и контекста. И заодно очистить компоненты от ручного управления загрузкой.

Что делает use()

Это специальный хук, который умеет читать значения из промисов и контекстов прямо в рендере. Если промис ещё не зарезолвлен — компонент «приостанавливается» (в смысле Suspense), и React показывает ближайший fallback. Когда промис готов — компонент рендерится с данными.

import { use } from 'react';

function Comments({ postId }: { postId: string }) {
  const comments = use(fetchComments(postId));
  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.body}</li>
      ))}
    </ul>
  );
}

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

Чем это отличается от useEffect

Старый паттерн «загрузить и показать» выглядел так:

function Comments({ postId }: { postId: string }) {
  const [data, setData] = useState<Comment[] | null>(null);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    let active = true;
    fetchComments(postId)
      .then((d) => { if (active) setData(d); })
      .catch((e) => { if (active) setError(e); });
    return () => { active = false; };
  }, [postId]);
  
  if (error) return <p>Ошибка</p>;
  if (!data) return <p>Загрузка...</p>;
  return <ul>{data.map((c) => <li key={c.id}>{c.body}</li>)}</ul>;
}

На use() та же логика становится одной строкой:

function Comments({ postId }: { postId: string }) {
  const data = use(fetchComments(postId));
  return <ul>{data.map((c) => <li key={c.id}>{c.body}</li>)}</ul>;
}

Лоадер и error boundary вынесены вверх:

<ErrorBoundary fallback={<p>Ошибка</p>}>
  <Suspense fallback={<p>Загрузка...</p>}>
    <Comments postId={postId} />
  </Suspense>
</ErrorBoundary>

Преимущества: меньше кода, меньше моргающих состояний, единый Suspense fallback на регион, нет race conditions с двойными запросами при перерендере.

Кеширование промисов — критично

Самый частый источник ошибок: каждый рендер создаёт новый промис, и React начинает гонять запрос по кругу. Решение — мемоизация:

import { use, useMemo } from 'react';

function Comments({ postId }: { postId: string }) {
  const promise = useMemo(() => fetchComments(postId), [postId]);
  const data = use(promise);
  return <ul>{data.map((c) => <li key={c.id}>{c.body}</li>)}</ul>;
}

Лучше — отдавать промис снаружи, из родителя:

function CommentsWrapper({ postId }: { postId: string }) {
  const promise = useMemo(() => fetchComments(postId), [postId]);
  return (
    <Suspense fallback={<Skeleton />}>
      <Comments promise={promise} />
    </Suspense>
  );
}

function Comments({ promise }: { promise: Promise<Comment[]> }) {
  const data = use(promise);
  return <ul>{data.map((c) => <li key={c.id}>{c.body}</li>)}</ul>;
}

Это позволяет начать загрузку до того, как Suspense покажет fallback — паттерн «render-as-you-fetch».

use() для контекстов

Второй сценарий: чтение контекста условно. useContext нельзя звать в условии, а use(SomeContext) — можно:

import { use } from 'react';
import { ThemeContext } from './ThemeContext';

function Card({ highlighted }: { highlighted: boolean }) {
  if (!highlighted) return <div className="card">...</div>;
  const theme = use(ThemeContext);
  return <div className={`card card--${theme}`}>...</div>;
}

Я этим часто пользуюсь в сложных компонентах с разветвлённой логикой: контекст читается только тогда, когда он нужен.

Что use() не умеет

  • Заменять useEffect для побочных эффектов: подписки, таймеры, внешние слушатели. Эффекты остаются эффектами.
  • Хранить состояние. Это не useState.
  • Запускать действия по событию. Использовать промис «по клику» через use нельзя — нужен обычный обработчик.

Сравнение с TanStack Query

Часто спрашивают: «зачем нам use(), если есть TanStack Query?». Они не конкурируют. use() — низкоуровневый примитив. TanStack Query — слой над ним, со своим кешем, ревалидациями, инвалидацией по тегам, optimistic updates.

В Next.js c App Router и серверных компонентах часто хватает use(): данные тянутся на сервере, в клиент уезжает уже готовый JSX, и проблема ревалидации решается через revalidateTag. Если приложение преимущественно клиентское и нужны сложные кеши — TanStack Query всё ещё выигрывает.

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

В RSC use() работает на сервере. То есть ты можешь писать промис прямо в серверном компоненте, и он будет «дожидаться» на сервере, не отдавая никакого Suspense на клиент:

// app/posts/page.tsx — серверный компонент
import { use } from 'react';

export default function Page() {
  const posts = use(db.post.findMany());
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

Это эквивалент const posts = await db.post.findMany(). Разница в том, что use можно встроить в любое место рендера и комбинировать с условиями.

Где я аккуратнее

На клиентских компонентах с use() легко создать каскад Suspense, при котором страница «дёргается» по мере подгрузки разных кусков. Лучше один Suspense на регион + параллельный запуск промисов сверху, чем несколько вложенных.

И не забывай про error boundary. Промис может упасть, и без обёртки страница покажется белой. У меня есть привычка всегда оборачивать Suspense в свой ErrorBoundary и логировать ошибку в Sentry прямо в onError.

В сухом остатке

В типичной задаче «получить данные и нарисовать» React 19 даёт три более простых способа, чем раньше: use(), серверные компоненты с await, server actions для мутаций. useEffect остаётся, но возвращается к своей исходной роли — побочные эффекты, не загрузка данных.

Если ты ещё пишешь useEffect(() => { fetch(...).then(setData) }, []) — пора посмотреть на use(). Кода становится в три раза меньше, и количество багов с race conditions падает до нуля.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.