React 19 `use()` 来了:以后数据加载可以不用 useEffect?

46 阅读2分钟

问题场景

假设你有一个普通的 React 组件,要从 API 加载用户数据:

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <Skeleton />;
  return <div>{user.name}</div>;
}

这段代码写了 15 行,就为干一件事:等数据回来再渲染。而且你还要处理:

  • 竞态(后发的请求覆盖了先发的)
  • 错误边界
  • 内存泄漏(卸载后还在 setState)

这些问题在过去几年催生了 React Query、SWR 等库。但 React 19 说:这活我能干了。

原因分析

React 团队在 19 版本引入了一个全新的 API——use()。它不是 Hooks,不能在条件/循环外的那套规则限制它(虽然实际上最好也别那么干)。它的签名是:

function use<T>(promise: Promise<T>): T;
function use<T>(context: Context<T>): T;

use() 做了什么?

  • 传入一个 Promise → 挂起组件直到 resolve,配合 <Suspense> 展示 fallback
  • 传入一个 Context → 等价于 useContext,但可以在条件语句和循环里用

关键区别:use() 在渲染期间直接消费 Promise,React 会帮你处理"等待→重渲染"的流程,不再需要 useEffect + useState 那套样板代码。

解决方案

1. 用 use() 替代 useEffect 数据加载

先写一个获取 Promise 的工具:

// fetchUser.ts
let cache = new Map<string, Promise<User>>();

export function fetchUser(id: string): Promise<User> {
  if (!cache.has(id)) {
    cache.set(id, fetch(`/api/users/${id}`).then(r => r.json()));
  }
  return cache.get(id)!;
}

然后在组件里直接用 use()

import { use, Suspense } from 'react';

function UserProfile({ userId }: { userId: string }) {
  const user = use(fetchUser(userId)); // 👈 直接等
  return <div>{user.name} — {user.email}</div>;
}

// 父组件包 Suspense
function Page({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}

15 行 → 3 行。竞态问题?React Suspense 天然解决——每次渲染都 use(fetchUser(userId)),如果 userId 变了,老的 Promise 被丢弃,新的挂起,不存在覆盖问题。

2. 那缓存怎么办?

上面的 fetchUser 用了最简的 Map 缓存。生产环境建议配合 React 19 的 cache()

import { cache } from 'react';

export const fetchUser = cache(async (id: string) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

cache() 保证同样的参数只调用一次函数,结果自动 dedupe。Server Components 里用这个极其丝滑。

3. use(context) — 条件中使用 Context

过去你不能在 if 里用 useContext,但用 use() 可以:

function Sidebar({ showTheme }: { showTheme: boolean }) {
  // ✅ 条件中读 Context
  const theme = showTheme ? use(ThemeContext) : 'light';

  // ✅ 循环中读 Context
  return items.map(item => {
    const config = use(ConfigContext);
    return <Item key={item.id} config={config} />;
  });
}

这对高阶组件、渲染函数场景非常实用。

要点总结

对比传统写法 (useEffect)use() + Suspense
代码量12–20 行3–5 行
竞态处理手动 cleanup天然解决
加载状态手动 stateSuspense fallback
错误处理手动 try/catch + stateErrorBoundary
条件中读 Context❌ 不行✅ 可以

⚠️ 注意:

  • use() 还在 Experimental,可以用 React 19 RC 尝鲜。生产环境等正式版再上。
  • use(promise) 必须在 <Suspense> 内部才能挂起,没有 Suspense 兜底会抛错。
  • 客户端组件中 use() + cache() 需要搭配 React 19 的数据获取方案,目前 Server Components 体验最成熟。
  • use() 不是什么都能替代——事件处理、定时器、副作用逻辑还是得用 useEffect

一句话总结: 如果你的组件只做一件事——"等数据,然后渲染",use() 是过去 5 年 React 数据加载最优雅的解法。再也不用为了一个 fetch 写一整套生命周期体操了。