React 并发特性


本文旨在了解 React 的关键并发特性——useTransitionuseDeferredValueSuspense 和 useOptimistic——以及它们如何协同工作以创建流畅、响应迅速的用户体验。包含实际示例和最佳实践。

并发渲染 (Concurrent Rendering)

React 的并发渲染是实现其他一切的基础。React 不会在渲染时阻塞浏览器,而是可以暂停、优先处理和协调不同类型的工作。在并发渲染之前,React 是同步工作的。当你触发更新时,React 会阻塞主线程,直到整个组件树重新渲染完成。这可能导致卡顿的交互——如果同时渲染开销大的组件,输入可能会感觉迟钝。并发渲染通过引入可中断渲染改变了这一点。React 可以开始渲染一个更新,暂停以处理更紧急的工作(如用户输入),然后从中断处恢复。

useTransition:协调工作

useTransition 将状态更新标记为非紧急,允许 React 中断它们以处理更重要的工作。所有状态更新在整个过渡(transition)完成后才会执行。React 19 引入了异步过渡(async transitions),允许你直接将异步函数传递给 startTransition。这些异步函数被称为“操作(Actions)”,应相应命名以区别于常规事件处理程序。

基本 API:

const [isPending, startTransition] = useTransition();

它不接受参数,并返回一个数组,包含 isPending(一个布尔值,指示是否有过渡正在进行)和 startTransition(一个用于将更新标记为非紧急的函数)。

用于开销大的渲染 (Expensive Rendering): 开销大的计算不应阻塞用户交互——过渡通过降低繁重工作的优先级来提供帮助。

这是一个标签按钮,使用过渡来防止在切换开销大的标签时造成阻塞:

function TabButton({ children, tabAction }{
  const [isPending, startTransition] = useTransition();

  const handleTabChange = () => {
    startTransition(() => tabAction());
  };

  return (
    <button onClick={handleTabChange} style={{ opacity: isPending ? 0.7 : 1 }}>
      {children}
    </button>

  );
}

注意这里的命名:属性 tabAction 遵循操作(Action)命名约定,向父组件发出信号,表明这是一个操作回调。

用于异步操作 (Async Operations): 对于 API 调用、表单提交和数据变更等异步操作,过渡可以协调状态更新与异步操作,从而防止 UI 闪烁。

这是一个删除按钮,在服务器处理请求时显示待处理状态:

function DeleteButton({ itemId }{
const [isPending, startTransition] = useTransition();

const handleDelete = () => {
    startTransition(async () => {
      await deleteItem(itemId);
    });
  };

return (
    <button
      onClick={handleDelete}
      disabled={isPending}
      style={{ opacity: isPending ? 0.7 : 1 }}
    >

      {isPending ? '删除中...' : '删除'}
    </button>

  );
}

注意,异步函数的 isPending 状态是自动提供的,避免了手动管理加载状态。

Suspense:声明式加载:Suspense 提供声明式的加载边界(loading boundaries)。它与 React.lazy() 一起用于代码分割(code splitting),并在启用 Suspense 的数据源(如异步服务器组件、与 use() API 一起使用的 promise、以及支持 Suspense 的库如 React Query 或 SWR)激活时工作。

API:

<Suspense fallback={<LoadingSkeleton />}>
  <Component />
</Suspense>

它接受一个 fallback 属性(加载期间显示的 JSX)和可能挂起(suspend)的子组件。在子组件全部准备就绪之前,它会渲染 fallback 内容。

用于代码分割 (Code Splitting): 当与 React.lazy 结合使用时,组件级代码分割变得声明式:

const LazyComponent = React.lazy(() => import('./HeavyComponent'));

function App({
  return (
    <Suspense fallback={<ComponentSkeleton />}>
      <LazyComponent />
    </Suspense>

  );
}

用于异步数据源 (Async Data Sources): 对于异步数据源,你可以将各个组件包装在它们自己的 Suspense 边界中。

function UserDashboard({ userId }{
  return (
    <div>
      <h1>仪表盘</h1>
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile userId={userId} />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts userId={userId} />
      </Suspense>
    </div>

  );
}

每个组件管理自己的数据,而父组件通过 Suspense 边界以声明式方式处理加载状态。

过渡与 Suspense 结合 (Transitions with Suspense):将过渡与 Suspense 结合可以防止导航期间出现突兀的加载状态。

这是一个路由器,在新页面后台加载时保持当前页面可见:

function AppRouter({
const [currentPage, setCurrentPage] = useState('home');
const [isPending, startTransition] = useTransition();

function navigateTo(page{
    startTransition(() => setCurrentPage(page));
  }

return (
    <div>
      <nav>
        {['home', 'profile', 'settings'].map(page => (
          <button key={page} onClick={() => navigateTo(page)}>
            {page}
          </button>
        ))}
      </nav>
      <Suspense fallback={<PageSkeleton />}>
        <div style={{ opacity: isPending ? 0.7 : 1 }}>
          <PageContent page={currentPage} />
        </div>
      </Suspense>
    </div>

  );
}

在 Next.js App Router 中,导航在底层会自动包装在过渡中。 注意:过渡只会“等待”足够长的时间以避免隐藏已经显示的内容。它们不会等待嵌套的 Suspense 边界。

use() API:读取 Promise:use API 是一个实用工具,与 Suspense 配合良好,用于读取 promise 和上下文值。与钩子(Hooks)不同,它可以有条件地调用,并且适用于支持 Suspense 的数据源或缓存的 promise。

基本 API:

const data = use(promise);
const contextValue = use(Context);

它接受一个 promise 或上下文(Context),并返回已解析的值。对于 promise,它会挂起组件直到解析完成。

这是一个完整的 Suspense + use API 示例:

function App({
const userPromise = fetchUser('/api/user/123');

return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>

  );
}

function UserProfile({ userPromise }{
const user = use(userPromise);
return<div><img src={user.avatar} /><h2>{user.name}</h2></div>;
}

use API 会挂起 UserProfile 组件直到 promise 解析,而 Suspense 在加载期间显示骨架回退内容。

useDeferredValue:智能延迟useDeferredValue 延迟渲染依赖于频繁变化值的 UI 部分,在当前内容可见的同时,保持当前内容可见,直到 React 有时间处理新值。

API:

const deferredValue = useDeferredValue(value);

它接受一个值,并返回一个延迟版本,该版本在快速更新期间会滞后。

用于开销大的渲染 (Expensive Rendering)当用户输入触发开销大的过滤或处理时,延迟值可以保持输入的响应性:

function FilteredList({ items }{
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter);

  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      <ExpensiveFilteredItems items={items} filter={deferredFilter} />
    </div>

  );
}

将开销大的组件用 memo 包装,以确保它仅在属性实际发生变化时才重新渲染:

const ExpensiveFilteredItems = memo(function ExpensiveFilteredItems({ items, filter }{
  return (
    <div>
      {items
        .filter(item => item.name.toLowerCase().includes(filter.toLowerCase()))
        .map(item => <div key={item.id}>{item.name}</div>)
      }
    </div>

  );
});

用于异步搜索操作 (Async Search Operations): 搜索界面受益于延迟值与支持 Suspense 的数据源的结合。

Suspense 在初始加载时提供回退内容,而延迟值在输入期间保持先前结果可见,防止每次按键时出现突兀的内容闪烁:

function SearchApp({
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);

const isStale = query !== deferredQuery;

return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <Suspense fallback={<div>搜索中...</div>}>
          <SearchResults query={deferredQuery} />
        </Suspense>
      </div>
    </div>

  );
}

function SearchResults({ query }{
if (!query) return<div>开始输入以搜索</div>;

const results = use(searchUsers(query));
return (
    <div>
      {results.map(user => <div key={user.id}>{user.name}</div>)}
    </div>

  );
}

isStale 模式通过比较当前查询词和延迟版本,向用户显示结果何时正在更新,使用不透明度来指示搜索何时跟上他们的输入。

useOptimistic:即时反馈:useOptimistic 立即显示乐观更新(optimistic update),而实际更新在后台进行。它必须在过渡(transition)中使用,以便 React 知道乐观状态应该存在多长时间。

API:

const [optimisticState, addOptimistic] = useOptimistic(currentState, updateFn);

它接受当前状态和一个更新函数,然后返回一个包含乐观状态和触发乐观更新的函数的数组。

用于 UI 交互 (UI Interactions)这是一个带有实时更新的点赞按钮:

function LikeButton({ post }{
const [, startTransition] = useTransition();
const [optimisticPost, setOptimisticPost] = useOptimistic(
    post,
    (currentPost, newLiked) => ({ ...currentPost, liked: newLiked, likes: currentPost.likes + (newLiked ? 1 : -1) })
  );

const toggleAction = () => {
    startTransition(async () => {
      setOptimisticPost(!optimisticPost.liked);
      await updatePostLike(post.id, !optimisticPost.liked);
    });
  };

return (
    <button onClick={toggleAction}>
      {optimisticPost.liked ? '❤️' : '🤍'} {optimisticPost.likes}
    </button>

  );
}

点击时,心形图标和计数会立即改变——不需要待处理状态,因为更新提供了反馈。

用于表单提交 (Form Submissions): 表单受益于乐观更新,可以在服务器后台处理时显示即时反馈。这个评论表单使用了 React 19 的表单操作(form actions),它会自动包装在过渡中:

function CommentForm({ comments, onAddComment }{
const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (currentComments, newComment) => [...currentComments, { ...newComment, isPostingtrue }]
  );
const formRef = useRef();

asyncfunction submitAction(formData{
    const comment = formData.get('comment');
    addOptimisticComment({ text: comment, idDate.now() });
    await onAddComment(comment);
    formRef.current.reset();
  }

return (
    <div>
      <form ref={formRef} action={submitAction}>
        <textarea name="comment" />
        <button type="submit">发布</button>
      </form>
      {optimisticComments.map(c => (
        <div key={c.id} style={{ opacity: c.isPosting ? 0.7 : 1 }}>
          {c.text} {c.isPosting && '(发布中...)'}
        </div>
      ))}
    </div>

  );
}

当你提交表单时,乐观评论会立即添加到列表中,并带有 isPosting: true 属性。此属性仅在表单操作过渡运行时存在——一旦服务器请求成功完成,乐观评论就会被真实的评论数据(没有 isPosting)替换。如果服务器请求失败,useOptimistic 会自动回滚并完全删除乐观评论。

结论:

  • useTransition() 创建较低优先级的状态更新,以在繁重处理和异步操作期间保持用户输入的响应性,并内置加载状态。
  • useDeferredValue() 延迟渲染依赖于频繁变化值的 UI 部分,在繁重渲染期间保持界面响应,并防止异步操作中出现突兀的内容闪烁。
  • Suspense 为代码分割和异步操作提供声明式的加载边界,协调服务器请求和加载状态。
  • useOptimistic() 通过异步操作的乐观更新,使用户交互感觉即时。

最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库已经更新1600多道面试题,除了八股文,还有现在面试官青睐的场景题,甚至最热的AI与前端相关的面试题已经更新,努力做全网最全最新的前端刷题网站。


有会员购买、辅导咨询的小伙伴,可以通过下面的二维码,联系我们的小助手。

图片