本文旨在了解 React 的关键并发特性——
useTransition、useDeferredValue、Suspense和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, isPosting: true }]
);
const formRef = useRef();
asyncfunction submitAction(formData) {
const comment = formData.get('comment');
addOptimisticComment({ text: comment, id: Date.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与前端相关的面试题已经更新,努力做全网最全最新的前端刷题网站。
有会员购买、辅导咨询的小伙伴,可以通过下面的二维码,联系我们的小助手。