React 19 use() хук: чем заменяет useEffect
Когда я первый раз увидел 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 падает до нуля.