
如果你还在 React 应用的 useEffect里写 fetch请求,那你就做错了。我们每天都听到这种说法。但我们应该怎么做才对呢?好吧,事实证明,React 团队已经清楚地听到了我们的声音。React 19.2 不仅仅是修补异步问题------它通过 use()、<Suspense>、useTransition()以及现在的 View Transitions,从头重建了异步处理机制。这些基础构建块共同将异步逻辑从一个"必要的麻烦"转变为一流的架构特性。让我带你了解 React 的异步叙事是如何彻底改变的,以及它为什么对你的团队很重要。
旧方式:useEffect + isLoading
让我们从一个展示旧的 useEffect/ fetch组合模式的例子开始:
javascript
function ImageViewer() {
const [imageId, setImageId] = useState(1);
const [imageData, setImageData] = useState(null);
const [isPending, setIsPending] = useState(false);
useEffect(() => {
setIsPending(true);
fetchImage(imageId).then((data) => {
setImageData(data);
setIsPending(false);
});
}, [imageId]);
return (
<div>
<button onClick={() => setImageId((id) => id + 1)}>Next Image</button>
{isPending ? <span>Loading...</span> : }
</div>
);
}
(这里的 fetchImage只是 fetch的一个包装函数,以保持示例简短。)
这个模式确实能用,但你做了很多重复性的工作。例如,你管理了三个状态(imageId, imageData, isPending),而实际上你只是想显示一张图片。加载状态逻辑是手动的,错误处理也常常是事后才考虑。而且很可能,你的代码库中每个异步操作看起来都略有不同。
最糟糕的是,那个 useEffect的依赖数组是个隐患。漏掉一个依赖项,你会得到过时的闭包;包含太多依赖项,又会触发无限循环。这是无数生产环境 bug 的根源。
这并不理想,老实说,用这种代码你能做到的最好程度就是"不搞砸"。而这并不是你想要编写的代码类型。
新的异步基础构建块
React 19.2 给我们提供了一种完全不同的方法。我们不再用 useEffect来管理 Promise,而是直接使用 use()和 <Suspense>来处理 Promise。
核心模式:use() + <Suspense>
下面是使用 React 新的 useHook 和 Suspense组件组合重写的同一个组件:
javascript
import { use, Suspense, useState } from "react";
function ImageViewer() {
const [imageId, setImageId] = useState(1);
const [imageDataPromise, setImageDataPromise] = useState(() => fetchImage(1));
const handleNext = () => {
const nextId = imageId + 1;
setImageId(nextId);
setImageDataPromise(fetchImage(nextId));
};
return (
<div>
<button onClick={handleNext}>Next Image</button>
<Suspense fallback={<ImageSkeleton />}>
<Image imageDataPromise={imageDataPromise} />
</Suspense>
</div>
);
}
function Image({ imageDataPromise }) {
const image = use(imageDataPromise);
return (
<div>
<h2>{image.title}</h2>
</div>
);
}
看看这有多简洁。没有 useEffect。没有手动的 isPending状态。父组件中没有条件渲染逻辑。我们存储的是一个 Promise 而不是数据,React 会处理剩下的事情。
但这是如何工作的呢?<Suspense>背后的魔法
当 <Suspense>尝试渲染其子组件时,<Image>组件会调用 use(imageDataPromise)。如果那个 Promise 还没有解决,use()会直接抛出这个 Promise。是的------像抛出异常一样抛出它。
我知道你在想什么:"用异常来控制流程?这太奇怪了!"但请听我说,因为这其实非常巧妙。
那个被抛出的 Promise 会向上冒泡穿过组件树,直到被 <Suspense>捕获。<Suspense>于是知道:"啊,我的子组件需要从这个 Promise 获取数据。我会显示我的加载状态(fallback),并等待这个 Promise 解决。"
当 Promise 解决后,<Suspense>会重新渲染其子组件,use()返回数据,图片就显示出来了。
这消除了在组件树的每个层级进行条件渲染的需要。加载状态变得声明式,因为你可以用 <Suspense>包装异步组件,并定义在加载时显示什么。React 会处理时机。
更流畅的交互:useTransition() + action
但我们可以让它变得更好。目前,当你点击 "Next Image" 时,按钮在新图片加载期间仍然可点击。这不是很好的用户体验;用户可能会多次点击,造成竞态条件。React 19.2 引入了 action props 和 useTransition()来优雅地处理这种情况:
javascript
function Button({ action, children }) {
const [isPending, startTransition] = useTransition();
const onClick = () => {
startTransition(async () => {
await action();
});
};
return (
<button onClick={onClick} disabled={isPending}>
{children}
</button>
);
}
function ImageViewer() {
const [imageId, setImageId] = useState(1);
const [imageDataPromise, setImageDataPromise] = useState(() => fetchImage(1));
const handleNext = async () => {
const nextId = imageId + 1;
setImageId(nextId);
setImageDataPromise(fetchImage(nextId));
};
return (
<div>
<Button action={handleNext}>Next Image</Button>
<Suspense fallback={<ImageSkeleton />}>
<Image imageDataPromise={imageDataPromise} />
</Suspense>
</div>
);
}
名为 action的 prop 本身并没有什么特别之处。它只是一个约定,告诉其他开发者这个按钮会将处理器包装在一个过渡(transition)中。
View Transitions:GPU 加速的润色
既然我们谈到了异步的 UI 部分,让我们聊聊 View Transitions。React 19.2(在实验性分支上)添加了对浏览器原生 View Transitions API 的支持。这让你在异步内容加载时能获得如黄油般顺滑的、GPU 加速的动画。而且只需要几行代码。
首先,添加一些 CSS 动画:
css
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
::view-transition-old(image-container) {
animation: fadeOut 0.3s ease-out;
}
::view-transition-new(image-container) {
animation: fadeIn 0.3s ease-in;
}
::view-transition-old(button-pulse) {
animation: pulse 0.3s ease-in-out;
}
然后将 View Transitions 添加到你的组件中:
javascript
import { experimental_useViewTransition as useViewTransition } from "react";
function Image({ imageDataPromise }) {
const image = use(imageDataPromise);
return ;
}
function ImageSkeleton() {
return <div className="image-skeleton" />;
}
function Button({ action, children, disabled }) {
const [isPending, startTransition] = useTransition();
const viewTransition = useViewTransition();
const onClick = () => {
startTransition(async () => {
await action();
});
};
return (
<div {...viewTransition("button-pulse")}>
<button onClick={onClick} disabled={isPending}>
{children}
</button>
</div>
);
}
function ImageViewer() {
const [imageId, setImageId] = useState(1);
const [imageDataPromise, setImageDataPromise] = useState(() => fetchImage(1));
const handleNext = async () => {
const nextId = imageId + 1;
setImageId(nextId);
setImageDataPromise(fetchImage(nextId));
};
return (
<div>
<Button action={handleNext}>Next Image</Button>
<div {...useViewTransition("image-container")}>
<Suspense fallback={<ImageSkeleton />}>
<Image imageDataPromise={imageDataPromise} />
</Suspense>
</div>
</div>
);
}
就这样。浏览器会自动处理 GPU 加速的过渡动画。你的骨架屏淡出,你的图片淡入,你的按钮在加载时会脉动。效果非常丝滑,而你几乎没写什么代码。
注意: 你需要 React 19.2 实验版才能使用此功能:npm install react@experimental react-dom@experimental
这对前端团队为何重要
这些改变不仅仅是清理 useEffect/ fetch的烂摊子。它关乎构建一个更优秀的 React,这个 React 终于承认异步是一等公民。
这些模式意味着你的团队编写的代码更容易理解和维护。框架处理了更多事情。UI 响应更迅捷,因为它是按需更新的。你正在利用更多底层框架的能力,为你的应用赋予具有流畅动画的现代感。
这不再是"你爷爷辈的 React"了,是时候让团队开始使用这些新工具了。
关键要点
-
React 19.2 带来了"无处不在的异步"。
-
这些基础构建块作为一个系统协同工作:
use()从 Promise 中提取数据<Suspense>声明式地处理加载状态useTransition()免费为你提供加载标志- View Transitions 添加 GPU 加速的润色
-
旧方式(
useEffect和fetch)对 React 来说是个严重的痛点,但我们有了很好的替代方案。 -
React 的异步基础构建块意味着更少的代码、更少的 bug 和更好的用户体验。
-
你的团队可以更专注于构建体验,而不是支撑它的底层管道。
非 19.2 的选择:TanStack Query
话虽如此,如果你还没准备好升级到 React 19.2,有一个很好的替代方案:TanStack Query(前身为 React Query)。它适用于任何 React 版本,并为你提供自动缓存、后台重新获取、乐观更新等功能。它是一个久经考验的解决方案,解决了与 React 19.2 异步基础构建块相同的问题,只是 API 不同。有充分的理由说明为什么这么多 React 应用都安装了 react-query。
下一步:React 19.2 引领的前端未来
React 19.2 已经发布。它是稳定的。你应该将其用于生产环境(除了仍在完善中的 View Transitions)。
异步变革已经到来。React 终于解决了它最大的痛点,框架也因此变得更好。
如果你还没有探索过 React 19.2,现在是时候了。在一个副项目上试试 use()和 <Suspense>,看看异步逻辑变得多么简单。你会惊讶于自己以前没有它是怎么过的。