【翻译】React 19.2:异步化变革终于到来

50 阅读7分钟

原文链接:blog.logrocket.com/react-19-2-…

作者:Jack Herrington

如果你在 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 的封装,旨在使示例简洁。

现在,这种模式虽然可行,但你做了大量重复的机械工作。例如,你需要管理三个状态(imageIdimageDataisPending),而实际目标只是展示一张图片。加载状态的逻辑是手动实现的,错误处理往往是事后才想到的。而且很可能,你代码库中每个异步操作的实现都略有不同。

最糟糕的是,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 错误追踪功能:

  1. 访问 logrocket.com/signup/ 获取应用程序 ID
  2. 通过 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>
  1. (可选)安装插件以实现与技术栈的深度集成:
    • Redux 中间件
    • NgRx 中间件
    • Vuex 插件