2026/1/15 17:28:30
网站建设
项目流程
招聘网站咋做,工程公司名称大全大气好听,同创企业网站建设,沧州网页重做构建异步 UI 向来都是一件非常困难的事情。导航操作将内容隐藏在加载指示器之后#xff0c;搜索框在响应无序到达时会产生竞态条件#xff0c;表单提交则需要手动管理每一个加载状态标志和错误信息。每个异步操作都迫使你手动进行协调。这不是一个性能问题#xff0c;而是一…构建异步 UI 向来都是一件非常困难的事情。导航操作将内容隐藏在加载指示器之后搜索框在响应无序到达时会产生竞态条件表单提交则需要手动管理每一个加载状态标志和错误信息。每个异步操作都迫使你手动进行协调。这不是一个性能问题而是一个协调问题。现在React 的原语声明式地解决了它。对于开发团队而言这标志着我们构建方式的一次根本性转变。React 不再需要每位开发者在每个组件中重新发明异步处理逻辑而是提供了标准化的原语来自动处理协调。这意味着更少的 Bug、更一致的用户体验以及更少的调试竞态条件的时间。React 的异步协调原语在 React Conf 2025上来自 React 团队的 Ricky Hanlon 演示的 Async React 示例展示了未来的可能性一个包含搜索、标签页和状态变更的课程浏览应用在快速网络下感觉即时在慢速网络下也能保持流畅。UI 更新自动协调不会闪烁。这不是一个新库而是 React 19 的协调 API 与 React 18 的并发特性 的结合。它们共同构成了 React 团队称之为“异步 ReactAsync React”的完整系统通过可组合的原语来构建响应式的异步应用程序useTransition跟踪待处理的异步工作。useOptimistic在状态变更期间提供即时反馈乐观更新。Suspense声明式地处理加载边界。useDeferredValue在快速更新期间保持稳定的用户体验。use()使数据获取和上下文读取变得声明式。理解这些部分如何协同工作是关键它使我们能从命令式的异步代码转向声明式的协调。问题手动的异步协调在这些原语出现之前开发者必须手动编排每一个异步操作。表单提交需要显式的加载和错误状态function SubmitButton() { const [isLoading, setIsLoading] useState(false); const [error, setError] useState(null); async function handleSubmit() { setIsLoading(true); setError(null); try { await submitToServer(); setIsLoading(false); } catch (e) { setError(e.message); setIsLoading(false); } } return ( div button onClick{handleSubmit} disabled{isLoading} {isLoading ? 提交中... : 提交} /button {error div错误{error}/div} /div ); }数据获取也遵循类似的命令式模式使用useEffectfunction UserProfile({ userId }) { const [user, setUser] useState(null); const [isLoading, setIsLoading] useState(true); const [error, setError] useState(null); useEffect(() { setIsLoading(true); setError(null); fetchUser(userId) .then(data { setUser(data); setIsLoading(false); }) .catch(e { setError(e.message); setIsLoading(false); }); }, [userId]); if (isLoading) return div加载中.../div; if (error) return div错误{error}/div; return div{user.name}/div; }每个异步操作都重复这个模式跟踪加载状态、处理错误、协调状态更新。当这种模式扩展到几十个组件时就会导致不一致的加载状态、被遗忘的错误处理以及难以调试的微妙竞态条件。原语详解Actions 自动跟踪异步工作React 19 引入了 Actions 来声明式地处理异步协调。将一个异步函数包装在startTransition中可以让 React 跟踪整个操作const [isPending, startTransition] useTransition(); function submitAction() { startTransition(async () { await submitToServer(); }); }isPending标志在 Promise 解决之前一直为true。React 会自动处理此状态并且在 Transition 中抛出的错误会冒泡到错误边界Error Boundary而不是在分散的try/catch块中处理你仍需自己处理预期的错误如验证失败。React 将在 Transition 中调用的任何函数被称为 “Action”。命名约定很重要为函数添加 “Action” 后缀表示它们运行在 Transition 中例如submitAction、deleteAction。以下是使用 Actions 重写的相同按钮function SubmitButton() { const [isPending, startTransition] useTransition(); function submitAction() { startTransition(async () { await submitToServer(); }); } return ( button onClick{submitAction} disabled{isPending} {isPending ? 提交中... : 提交} /button ); }另一种选择是使用 React 19 的form组件它可以通过接受一个action属性并将其自动包装在 Transition 中来为你处理async function submitAction(formData) { await submitToServer(formData); } form action{submitAction} input nameusername / button提交/button /form;与手动 Action 一样错误仍会冒泡到错误边界。当你希望在 UI 中反映表单状态时React 19 提供了表单实用程序useFormStatus让子组件可以访问表单的待处理状态而useActionState则允许你根据 Action 的结果更新组件状态例如显示验证错误或“点赞”计数。相同的模式也适用于按钮、输入框和标签页等可复用组件。你的设计组件可以暴露action、submitAction或changeAction等 Action 属性并在内部使用 Transitions 来管理待处理状态和其他异步行为。我们稍后将回到这个模式。乐观更新提供即时反馈Actions 提供了待处理状态但“待处理”并不总是正确的反馈。当你点击复选框来标记任务完成时它应该立即切换。等待服务器的响应很可能会破坏流程导致竟态问题。useOptimistic()在 Transitions 内部工作用于在异步 Action 在后台运行时显示即时更新function CompleteButton({ complete }) { const [optimisticComplete, setOptimisticComplete] useOptimistic(complete); const [isPending, startTransition] useTransition(); function completeAction() { startTransition(async () { setOptimisticComplete(!optimisticComplete); await updateCompletion(!optimisticComplete); }); } return ( button onClick{completeAction} className{isPending ? opacity-50 : } {optimisticComplete ? CheckIcon / : div/div} /button ); }复选框会立即切换。如果请求成功服务器状态将与乐观更新匹配。如果失败服务器状态保持旧值因此复选框会自动恢复其原始状态。与useState它会延迟 Transition 内部的更新不同useOptimistic会立即更新。Transition 边界定义了乐观状态的生命周期它仅在异步 Action 处于待处理状态时持续存在一旦 Transition 完成就会自动“落定”到事实来源props 或服务器状态。注简单说就是当 transition 为 pending 时 optimisticComplete 为 startTransition 中设定的值而一旦 transition 完成即 pending 为 false 时optimisticComplete 会放弃 startTransition 的状态而使用传入的值及为例子中的 completeSuspense 声明式地协调加载状态乐观更新处理了状态变更但初始数据加载呢useEffect模式迫使我们手动管理isLoading状态。Suspense通过允许我们声明式地定义加载边界来解决这个问题。我们需要控制显示什么后备 UI 以及如何分割加载因此应用的独立部分可以并行加载。Suspense 与“支持 Suspense”的数据源协同工作异步服务器组件、使用use()API 读取的 Promise我们接下来会介绍以及像 TanStack Query 这样的库它提供了用于缓存和去重的useSuspenseQuery。以下是 Suspense 如何协调多个独立数据流function App() { return ( div h1仪表板/h1 Suspense fallback{ProfileSkeleton /} UserProfile / /Suspense Suspense fallback{PostsSkeleton /} UserPosts / /Suspense /div ); }每个组件都可以通过自己的后备方案独立挂起。父组件通过 Suspense 边界处理加载状态而不是协调多个useEffect调用。但有个问题当你触发导致组件重新获取数据的更新时如切换标签页或导航加载后备方案会再次显示隐藏你已经看到的内容并产生突兀的加载状态。结合 Transition 与 Suspense将 Transition 与 Suspense 结合可以解决这个问题它告诉 React 保持现有内容可见而不是立即再次显示后备方案。以下是一个针对标签页切换的适配示例function App() { const [tab, setTab] useState(profile); const [isPending, startTransition] useTransition(); function handleTabChange(newTab) { startTransition(() setTab(newTab)); } return ( div nav button onClick{() handleTabChange(profile)}个人资料/button button onClick{() handleTabChange(posts)}帖子/button /nav Suspense fallback{LoadingSkeleton /} div style{{ opacity: isPending ? 0.7 : 1 }}{tab profile ? UserProfile / : UserPosts /}/div /Suspense /div ); }现在加载后备方案仅在初始加载时显示。当你切换标签页时Transition 会在新数据在后台加载时保持当前内容可见。不透明度样式使其变暗以表示更新正在进行。一旦就绪React 会自动无缝地换入新内容。没有突兀的加载状态没有卡顿。关键在于Transitions 会“暂缓”UI 更新直到异步工作完成从而防止 Suspense 边界在导航期间回退到后备状态。像 Next.js 这样的框架使用此功能在新路由加载时保持页面可见。use()直接读取异步数据早些时候我们看到了 Suspense 如何与“支持 Suspense”的数据源协同工作。classnolinkuse() API 就是这样的数据源之一它为数据获取提供了 useEffect 的替代方案允许你在渲染期间读取 Promise。以下是用 Suspense 和 use() 重写的最初的 useEffect 示例function UserProfile({ userId }) { const user use(fetchUser(userId)); return div{user.name}/div; } function App({ userId }) { return ( ErrorBoundary fallback{div加载用户时出错/div} Suspense fallback{div加载中.../div} UserProfile userId{userId} / /Suspense /ErrorBoundary ); }组件在读取 Promise 时挂起触发最近的 Suspense 边界然后在 Promise 解决时带着数据重新渲染。错误被错误边界捕获。与 Hooks 不同use() 可以条件调用。一个注意事项Promise 需要被缓存。否则每次渲染都会重新创建它。在实践中你可以使用像 Next.js 这样处理缓存和去重的框架。延迟值防止 UI 过载Actions 和 Suspense 处理离散的操作点击、提交、导航。但快速输入如搜索需要不同的方法因为你希望输入框即使在结果加载时也能保持响应。一种方法可以是设计一个 SearchInput 组件通过内部乐观状态保持输入响应并在 Transition 中调用 changeAction这样父组件只需传递 value 和 changeAction。当你没有设计组件时useDeferredValue() 提供了类似的拆分效果。虽然你可以用它来延迟昂贵的 CPU 计算性能但此处的目标是稳定的用户体验。结合 Suspense、use() 和ErrorBoundary我们可以获得完整的搜索体验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)} / ErrorBoundary fallback{div加载结果时出错/div} Suspense fallback{div搜索中.../div} div style{{ opacity: isStale ? 0.5 : 1 }} SearchResults query{deferredQuery} / /div /Suspense /ErrorBoundary /div ); } function SearchResults({ query }) { if (!query) return div开始输入以搜索/div; const results use(fetchSearchResults(query)); return ( div {results.map(r ( div key{r.id}{r.name}/div ))} /div ); }Suspense 后备方案仅在初始加载时显示。在后续搜索期间useDeferredValue 会在新结果于后台加载时保持旧结果可见通过 isStale 降低不透明度。错误边界隔离了失败即使数据请求失败搜索输入也能保持功能正常。综合应用Async React 示例到目前为止我们分别了解了每个原语。Async React 展示了当一个框架将它们整合到路由、数据获取和设计系统中时会发生什么尝试切换网络速度以查看 UI 如何适应在快速连接下即时在慢速连接下流畅。路由器将导航包装在 Transitions 中function searchAction(value) { router.setParams(q, value); }更新搜索参数是异步的会更改 URL 并触发数据重新获取同时 Transition 会跟踪这一切。数据层将use()与缓存的 Promise 结合使用function LessonList({ tab, search, completeAction }) { const lessons use(data.getLessons(tab, search)); return ( Design.List {lessons.map(item ( Lesson item{item} completeAction{completeAction} / ))} /Design.List ); }当数据加载时组件会挂起Suspense 在初始加载时显示后备方案但在切换标签页和搜索期间Transitions 会保持旧内容可见。Design 组件暴露 Action 属性Design.SearchInput value{search} changeAction{searchAction} /SearchInput 在内部使用 useOptimistic以便在新的 URL 的 Transition 处于待处理状态时立即更新输入值。TabList 同样乐观地更新选中的标签页。命名约定“changeAction”表示传递的函数将在 Transition 中运行。状态变更以相同方式工作async function completeAction(id) { await data.mutateToggle(id); router.refresh(); }这个 completeAction 通过 LessonList 传递给 Design.CompleteButton该按钮也暴露了一个 action 属性。该按钮在 Action 运行时乐观地更新完成状态。这是一个简化版的课程应用示例export default function Home() { const router useRouter(); const search router.search.q || ; const tab router.search.tab || all; function searchAction(value) { router.setParams(q, value); } function tabAction(value) { router.setParams(tab, value); } async function completeAction(id) { await data.mutateToggle(id); router.refresh(); } return ( Design.SearchInput value{search} changeAction{searchAction} / Design.TabList activeTab{tab} changeAction{tabAction} Suspense fallback{Design.FallbackList /} LessonList tab{tab} search{search} completeAction{completeAction} / /Suspense /Design.TabList / ); }协调发生在每个层面路由导航被包装在 Transitions 中。数据获取数据层使用 Suspense 和缓存的 Promise。设计组件组件暴露“Action”属性以在内部处理乐观更新。在快速网络上更新是即时的。在慢速网络上乐观 UI 和 Transitions 在没有手动逻辑的情况下保持响应性。原语的复杂性由路由器、数据获取设置和设计系统处理。应用代码只需将它们连接起来。构建自定义异步组件大多数应用可能会使用已经实现了这些模式的库中的组件。但你也可以自己实现它们来构建自定义异步组件。这是一个针对 Next.js 的实用示例一个与 URL 参数同步的可复用选择组件。这对于过滤器、排序或任何你希望持久化在 URL 中的 UI 状态很有用import { useRouter, useSearchParams } from next/navigation; export function RouterSelect({ name, value, options, selectAction }) { const [optimisticValue, setOptimisticValue] useOptimistic(value); const [isPending, startTransition] useTransition(); const router useRouter(); const searchParams useSearchParams(); function changeAction(e) { const newValue e.target.value; startTransition(async () { setOptimisticValue(newValue); await selectAction?.(newValue); const params new URLSearchParams(searchParams); params.set(name, newValue); router.push(?${params.toString()}); }); } return ( select name{name} value{optimisticValue} onChange{changeAction} style{{ opacity: isPending ? 0.7 : 1 }} {options.map(opt ( option key{opt.value} value{opt.value} {opt.label} /option ))} /select ); }该组件在内部处理协调。父组件可以通过 selectAction 注入副作用function Filters() { const [progress, setProgress] useState(0); const [optimisticProgress, incrementProgress] useOptimistic(progress, (prev, increment) prev increment); return ( LoadingBar progress{optimisticProgress} / RouterSelect namecategory selected{selectedCategory} options{categoryOptions} selectAction{items { incrementProgress(30); setProgress(100); }} / / ); }在这个例子中进度条的乐观更新和路由器导航被协调在一起。传递给 selectAction 的任何内容都受益于相同的异步协调。命名约定“Action”表示它在 Transition 中运行并且我们可以在内部调用乐观更新。这就是 Async React 示例中设计组件使用的模式。SearchInput、TabList 和 CompleteButton 都暴露了 Action 属性在内部处理 Transitions、乐观更新和待处理状态。使用 ViewTransitionCanary实现平滑动画原语解决了更新何时发生的问题而 ViewTransition 则解决了它们看起来如何的问题。它包装了浏览器的View Transition API并专门在 React Transition由 useTransition、useDeferredValue 或 Suspense 触发内部更新组件时激活。默认情况下它在状态之间进行交叉淡入淡出你也可以使用 CSS 自定义动画。以下是 Async React 示例如何使用它为课程列表添加动画return ( ViewTransition keyresults defaultnone enterauto exitauto Design.List {lessons.map(item ( ViewTransition key{item.id} Lesson item{item} completeAction{completeAction} / /ViewTransition ))} /Design.List /ViewTransition );外层的 ViewTransition 在 Suspense 解析或在状态之间切换时如显示“无结果”为整个列表添加动画。每个项目上的内层 ViewTransition 为单个课程添加动画搜索时现有项目滑动到新位置而新项目淡入移除的项目淡出。注意ViewTransition 目前仅在 React 的 canary 版本中可用。实际权衡采用这些模式通常比它们所替代掉的手动逻辑更简单。你并没有增加复杂性而是将协调工作丢给了 React。话虽如此以 Transitions、乐观更新和 Suspense 边界的方式思考确实需要思维转变何时适用这些原语在具有丰富交互性的应用中表现出色仪表板、管理面板和搜索界面。它们消除了整类的 Bug。竞态条件消失了。导航感觉无缝。你可以用更少的样板代码获得“原生应用”的感觉。不要修复未损坏的东西如果 useState 和 useEffect 对你来说工作可靠就没有必要拆除它们。如果你没有在处理竞态条件、突兀的加载状态或输入延迟你就不需要解决不存在的问题。迁移路径你可以选择渐进式的采用。下次构建具有复杂异步状态的功能时可以尝试用 Transition 代替另一个 isLoading 标识。在即时反馈重要的地方添加乐观 UI。这些工具与现有代码共存因此你可以逐个功能地采用它们。结论向声明式异步的转变异步 ReactAsync React是并发渲染和协调原语的结合形成了一个用于处理异步工作的完整系统而这在过去需要手动编排。随着这些原语在整个生态系统中被采用这种转变变得切实可行。在 React Conf 2025 上宣布的 Async React 工作组 正在积极致力于在路由器、数据获取库和设计组件中标准化这些模式。我们已经看到它的实际应用路由器如 Next.js默认将导航包装在 Transitions 中。数据库如 TanStack Query 和 SWR深度集成了对 Suspense 的支持。设计系统预计将跟进暴露 Action 属性以在内部处理待处理状态和乐观更新。最终这将异步处理的复杂性从应用代码转移到了框架。你描述应该发生什么Action、状态变更、导航而 React 协调它如何发生待处理状态、乐观更新、加载边界。React 的下一个时代不仅是关于新功能更是关于让无缝的异步协调成为应用功能的默认方式。