原文链接:blog.logrocket.com/react-19-2-…
如果你在 React 应用的 useEffect 中使用 fetch 进行异步请求,那你就搞错了。我们每天都在听到这种情况。但我们应该怎么做才对呢?
事实证明,React团队已经听到了我们的呼声。React 19.2不仅修复了异步问题,更通过 use()、<Suspense>、useTransition()以及全新的视图过渡功能,从根本上重构了异步处理机制。
这些基础组件共同作用,将异步逻辑从不得不忍受的恶变为一流的架构特性。接下来我将带您了解 React 异步机制如何彻底革新,以及这对您的团队为何至关重要。
旧方法: useEffect + isLoading
让我们从一个展示旧版 useEffect/fetch 组合的经典案例开始:
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> : <img src={imageData} />}
</div>
);
}
在此情况下,fetchImage 只是对 fetch 的封装,旨在使示例简洁。
现在,这种模式虽然可行,但你做了大量重复的机械工作。例如,你需要管理三个状态(imageId、imageData、isPending),而实际目标只是展示一张图片。加载状态的逻辑是手动实现的,错误处理往往是事后才想到的。而且很可能,你代码库中每个异步操作的实现都略有不同。
最糟糕的是,useEffect依赖数组堪称地雷阵。漏加依赖会导致闭包失效,添加过多依赖则可能引发无限循环,这正是无数生产环境故障的根源。
这种做法并不理想,坦白说,你最多只能做到不搞砸它——而这绝非你理想中的代码形态。
新的异步原语
React 19.2 为我们提供了截然不同的方法。我们不再通过 useEffect 管理 Promise,而是直接使用 use() 和 <Suspense> 处理 Promise。
use()+:核心模式
以下是使用 React 新的 use Hook 和 Suspense 组件组合重写的相同组件:
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 }: { imageDataPromise: Promise<ImageData> }) {
const image = use(imageDataPromise);
return (
<div>
<h2>{image.title}</h2>
<img src={image} alt={image.title} />
</div>
);
}
看看这段代码多么简洁。没有 useEffect ,没有手动维护 isPending 状态,父组件里也没有条件渲染逻辑。我们存储的是Promise而非数据,其余工作都交给React处理。
但这究竟是如何实现的?
<Suspend> 背后的魔力
当 <Suspense> 尝试渲染其子组件时,<Image> 组件会调用 use(imageDataPromise)。若该Promise尚未解析,use()会直接抛出该Promise。没错——直接抛出——就像抛出异常一样。
我知道你在想什么:"用异常控制流程?太奇怪了!"但请听我说,这设计其实相当精妙。
抛出的 Promise 会沿组件树向上传播,直到被 <Suspense> 捕获。此时 <Suspense> 便会意识到:"啊,我的子组件需要这个 Promise 的数据。我先显示备用内容,等待 Promise 解析。" 当 Promise 解析完成后,<Suspense> 会重新渲染其子组件,use() 返回数据,图片随即显示。
这彻底消除了组件树各层级中条件渲染的需求。加载状态也变得声明式——只需将异步组件包裹在 <Suspense> 中,定义加载期间的显示内容,React会自动处理时机控制。
useTransition() + 动作:更流畅的交互体验
但我们可以做得更好。目前点击“下一张图片”按钮时,按钮在加载新图片期间仍保持可点击状态。这种用户体验并不理想:用户可能多次点击,从而引发竞争条件:
React 19.2 引入了 action 属性 和 useTransition() 方法,可优雅地处理此问题:
function Button({
action,
children,
}: {
action: () => Promise<void>;
children: React.ReactNode;
}) {
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 属性本身并没有什么特别之处。这只是一个约定,用来告诉其他开发者:这个按钮会将处理器包裹在过渡效果中。
视图过渡:GPU加速优化
既然我们已经进入异步操作的用户界面部分,接下来就来谈谈视图过渡。React 19.2(实验分支)新增了对浏览器原生视图过渡 API 的支持。这能在加载异步内容时提供丝般顺滑、GPU 加速的动画效果,且仅需几行代码即可实现。
首先,添加一些 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;
}
然后为组件添加视图过渡效果:
import { experimental_useViewTransition as ViewTransition } from "react";
function Image({ imageDataPromise }: { imageDataPromise: Promise<string> }) {
const image = use(imageDataPromise);
return <img src={image.url} className="image-container" />;
}
function ImageSkeleton() {
return (
<div className="image-skeleton" />
);
}
function Button({
action,
children,
disabled,
}: {
action: () => Promise<void>;
children: React.ReactNode;
disabled: boolean;
}) {
const [isPending, startTransition] = useTransition();
const onClick = () => {
startTransition(async () => {
await action();
});
};
return (
<ViewTransition name="button-pulse">
<button onClick={onClick} disabled={isPending}>
{children}
</button>
</ViewTransition>
);
}
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>
<ViewTransition name="image-container">
<Suspense fallback={<ImageSkeleton />}>
<Image imageDataPromise={imageDataPromise} />
</Suspense>
</ViewTransition>
</div>
);
}
就这样。浏览器会自动处理GPU加速的过渡效果。你的骨架逐渐淡出,图片逐渐淡入,按钮在加载时闪烁。效果丝般顺滑,而你几乎没写什么代码:
注意:此操作需要 React 19.2 实验版:npm install react@experimental react-dom@experimental
为什么这对前端团队很重要
这些变更不仅在于清理 useEffect/fetch 的混乱局面,更在于打造一个更优秀的 React——它终于将异步处理视为首要关注点。
这些模式意味着团队编写的代码更易于理解和维护。框架承担了更多工作,界面响应更迅捷,因为它实现了按需更新。同时,您正充分利用底层框架,为应用赋予现代感与流畅动画效果。
这已非昔日的 React,团队是时候升级到这些新工具了。
关键要点
React 19.2 实现了"无处不在的异步"。这些基础组件协同构成完整系统:
use()从 Promise 中提取数据<Suspense>以声明式方式处理加载状态useTransition()自动提供加载状态标记- 视图过渡效果增添 GPU 加速的视觉润色
传统方案(useEffect + fetch)曾让 React 头疼不已,如今我们拥有更优解。React 的异步原语代码更精简、错误更少、用户体验更佳。
团队能更专注于构建核心体验,而非支撑它的底层管道。
非19.2选项:TanStack查询
话虽如此,若您尚未准备好升级至 React 19.2,我们提供了一个绝佳的替代方案:TanStack Query(原 React Query)。
以下是使用 TanStack Query 实现的相同组件:
TanStack Query 兼容任何 React 版本,并提供自动缓存、后台重新加载、乐观更新等功能。这是经过实战检验的解决方案,能解决与 React 19.2 异步原语相同的问题,只是采用不同的 API 设计。众多 React 应用安装 react-query 绝非偶然。
下一步:React 19.2 引领的前端未来
React 19.2 已发布。该版本已稳定,建议用于生产环境(除仍在优化的视图过渡功能外)。异步编程时代已然到来。React 终于解决了其最大的痛点,框架因此更臻完善。
若您尚未体验 React 19.2,此刻正是良机。不妨在副项目中尝试 use() 和 <Suspense>,感受异步逻辑如何变得简洁明了。您会惊叹于此前没有它们的日子是如何度过的。
只需几分钟,即可配置 LogRocket 的现代化 React 错误追踪功能:
- 访问 logrocket.com/signup/ 获取应用程序 ID
- 通过 npm 或脚本标签安装 LogRocket。LogRocket.init() 必须在客户端调用,而非服务器端。
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
- (可选)安装插件以实现与技术栈的深度集成:
- Redux 中间件
- NgRx 中间件
- Vuex 插件