Featured image of post React 并发功能:构建响应式 UIFeatured image of post React 并发功能:构建响应式 UI

React 并发功能:构建响应式 UI

掌握 React 并发功能:useTransition、useDeferredValue、Suspense、startTransition 以及使用 React 18+ 构建响应式输入组件。

React 18 引入了并发功能,从根本上改变了渲染的工作方式。这些功能使 React 可以一次准备多个版本的 UI、中断正在进行的工作,并优先考虑紧急更新而不是非紧急更新。结果是应用程序响应速度更快,而无需放弃使 React 高效的声明式编程模型。

React 中的并发不是一种全有或全无的模式。与早期实验版本中废弃的“并发模式”概念不同,React 18+ 选择加入并发功能。您可以逐个采用它们,因为它们可以提供最大的价值。底层 Fiber 架构使这成为可能:React 的渲染阶段可以暂停和恢复,因此协调器可以随着优先级的变化在不同的工作单元之间切换。

使用转换钩子

useTransition 挂钩可让您将状态更新标记为非紧急。这可以防止更新在关键用户交互(例如打字)期间阻塞主线程。

import { useTransition } from "react";

function SearchPage() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");

  function handleChange(e) {
    // Urgent: update the input value immediately
    const value = e.target.value;
    setQuery(value);

    // Non-urgent: filter the large dataset
    startTransition(() => {
      setFilter(value);
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <SearchResults filter={filter} />
    </div>
  );
}

isPending 标志提供了一种在不阻塞 UI 的情况下显示加载指示器的方法。当 React 正在进行转换时,isPendingtrue,用户可以继续键入,因为转换可能会被更紧急的状态更新中断。

转换与使用 setTimeout 进行去抖动有根本的不同。转换根本不会延迟执行。它降低了渲染工作的优先级,允许 React 暂停它,检查更高优先级的工作,并在线程空闲时恢复。去抖动总是会引入固定的延迟;当 CPU 空闲时,转换会引入零延迟。


useDeferredValue 钩子

其中 useTransition 包装状态设置器, useDeferredValue 包装值。当您无法轻松包装状态更新本身但仍希望保持 UI 响应时,这非常有用。

import { useDeferredValue, useMemo } from "react";

function SearchResults({ query, data }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  const filtered = useMemo(
    () => expensiveFilter(data, deferredQuery),
    [deferredQuery, data]
  );

  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {filtered.map((item) => (
        <ListItem key={item.id} item={item} />
      ))}
    </div>
  );
}

当 CPU 空闲时,延迟值会立即更新,而不会在固定延迟之后更新。这会创建一个“陈旧状态”模式:用户在计算新结果时看到以前的结果(稍微变暗),而不是空白的加载旋转器。这种模式提供了一种感觉上更流畅的体验,因为 UI 永远不会变空。

方法延迟用户体验
同步渲染每次击键都充满UI 在过滤期间冻结
去抖(300 毫秒)固定 300 毫秒延迟输入感觉滞后
占位符_0空闲时零延迟反应灵敏输入,平滑过渡

并发渲染的悬念

在并发渲染下,悬念变得更加强大。在同步模型中,当组件挂起时,Suspense 边界上方的整个树都会回落到加载状态。通过并发功能,同级组件在解析挂起的组件时保持交互。

function Page() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Sidebar />
      <Suspense fallback={<DetailSkeleton />}>
        <MainContent />
      </Suspense>
    </Suspense>
  );
}

嵌套 Suspense 边界可让您控制加载状态的粒度。 Sidebar 可以在 MainContent 流入时立即渲染。与 startTransition 结合使用,您可以在加载下一个屏幕时显示上一个 UI,从而消除困扰传统导航的“加载闪现”。


startTransition 外部钩子

startTransition 函数也可以作为独立导入在 React 组件之外使用。这对于状态管理库、路由器事件处理程序以及任何钩子不可用的代码路径都很有价值。

import { startTransition } from "react";
import { useNavigate } from "react-router-dom";

function navigateWithTransition(path) {
  startTransition(() => {
    // Wrap the navigation in a transition
    // so the previous view stays visible
    navigate(path);
  });
}

您可以将 Zustand 或 Redux 存储更新包装在 startTransition 中,以降低非紧急状态更改的优先级。当存储更新触发许多订阅者之间昂贵的派生计算时,这尤其有用。


构建响应式搜索组件

实用的搜索组件将所有并发功能组合在一起。输入状态紧急。过滤器计算使用 useDeferredValue。导航到详细信息页面使用路由器上的 startTransition 。搜索结果本身被包裹在悬念边界中。

这是一个完整的模式:

function SearchContainer() {
  const [input, setInput] = useState("");
  const deferredInput = useDeferredValue(input);

  function handleClear() {
    setInput("");
    // The deferred value will catch up naturally
  }

  return (
    <Suspense fallback={<ResultsSkeleton />}>
      <SearchInput
        value={input}
        onChange={setInput}
        onClear={handleClear}
      />
      <SearchResults query={deferredInput} />
    </Suspense>
  );
}

当用户快速键入时,只有最后一次击键才会触发完整渲染。所有中间击键都会中断之前的转换。这避免了“卡顿搜索”问题,即打字感觉缓慢,因为每次击键都会触发昂贵的过滤器通过。


高级模式和最佳实践

对于分层优先级控制,请将 useTransitionuseDeferredValue 组合起来。使用 useTransition 进行导航级状态更改(选项卡切换、页面转换),使用 useDeferredValue 进行视图内的派生计算。

要测试并发功能,请使用带有 act 包装器的 React 测试库:

import { act, render, screen } from "@testing-library/react";

await act(async () => {
  render(<SearchPage />);
});

// Assert on the pending state
expect(screen.getByText("Loading...")).toBeInTheDocument();

优化前的配置文件。 React DevTools Profiler 显示渲染是否被中断以及每个渲染阶段消耗了多少时间。过早使用并发功能会增加复杂性,而不会带来可衡量的好处 - 始终首先进行测量,然后在解决真正瓶颈的地方应用转换和延迟值。


兼容性和迁移

并发功能需要 React 18+ 并且完全可以选择加入。升级时不会破坏现有组件。建议的迁移路径从高延迟用户交互开始:搜索输入、过滤大型列表和选项卡切换。一旦这些顺利进行,就扩展到导航转换和数据加载模式。

一些同步改变状态的第三方库可能不尊重转换边界。在这些上下文中采用转换之前测试库兼容性。

概括

并发功能为您提供了三个强大的工具:用于取消状态更新优先级的 useTransition、用于推迟派生计算的 useDeferredValue 以及用于优雅加载状态的并发 Suspense。每个解决特定类别的响应问题。首先进行分析,选择正确的工具,并提供即使在繁重的计算下也感觉即时的 UI。