配了三年 React,每次切页都要自己管动画——今天发现它把整件事封装成了组件
每次在 React 里做页面过渡动画,都要自己调 document.startViewTransition()、套 flushSync、担心 React 批量更新打乱时序——这套组合拳我配了三年。今天 React 19 把这个流程彻底封装成了 <ViewTransition> 组件,三行代码解决战斗。
这件事的根子在于:React 的异步批量更新和浏览器 View Transition API 的同步快照是天然冲突的。你在 startViewTransition 回调里调 setState,React 不一定会在回调结束前把 DOM 同步 flush 出去——结果是浏览器拿到的「前后快照」内容一样,过渡动画变成两张相同的静态图交叉淡入淡出。
React 19 之前解决这个问题只有一条路:flushSync 强制同步更新。这条路能用,但丑,每次写路由都要惦记这个细节。
<ViewTransition> 把这个细节封装掉了。它的工作原理很直接:给两个页面的同名元素加上相同的 name 属性,React 在路由切换时自动调 document.startViewTransition(),内部处理 flushSync 同步问题。
// 列表页
<img
src={movie.poster}
style={{ viewTransitionName: `poster-${movie.id}` }}
/>
// 详情页
<img
src={movie.poster}
alt={`${movie.title} poster`}
style={{ viewTransitionName: `poster-${movie.id}` }}
/>
两个元素 viewTransitionName 相同,React 自动识别为「同一个元素的跨页过渡」,浏览器在导航时自动把它们 morph 起来——从列表里的卡片尺寸「飞」到详情页的大图,全程无需写一行 JS 动画逻辑。
多个元素同时过渡也没问题:
<ViewTransition name={`title-${movie.id}`}>
<h1>{movie.title}</h1>
</ViewTransition>
<ViewTransition name={`poster-${movie.id}`}>
<img src={movie.poster} alt={movie.title} />
</ViewTransition>
标题和海报同时飞,浏览器并行处理两组动画。
默认是交叉淡入淡出,想自定义的话直接写 CSS pseudo-elements:
::view-transition-old(poster-123),
::view-transition-new(poster-123) {
animation-duration: 500ms;
}
::view-transition-new(poster-123) {
animation: slide-in 0.4s ease-out;
}
Next.js 16 + React 19 的集成更省心:配置 experimental.viewTransition: true 之后,<Link> 点击自动触发过渡,连 startViewTransition 都不用调。但要注意一点:Next.js 的 router.push() 异步执行,transition callback 在 push 完成前就 resolve 了——结果是旧页面淡出后停在加载态,白等。要绕过这个问题,官方建议用 await transition.ready 等动画真正跑起来再 push。
React 19.2 里这个组件还叫 unstable_ViewTransition,预计 19.3 去掉前缀。底层浏览器 API 本身已经稳定,Chrome 111+、Safari 18.2+ 都支持同页过渡,跨文档过渡(整页跳转)需要 Chrome 126+。
下一步:如果你用 Next.js 15/16,开一下 experimental.viewTransition 试试水;如果你是纯 React SPA,把动画组件用 <ViewTransition name=”xxx”> 包起来,感受一下「写两行 props 等浏览器自动处理」的体验——用过之后,你不会再想回去调 flushSync 那套了。
评论区
登录后可评论。