React 基础体系 · 第 41/70 篇。示例以 React 19、现代 TypeScript 和主流框架能力为基础;客户端与服务端边界会明确说明。
React 动画:CSS、Transition、Motion、布局动画和减少动态
React 本身不负责“把元素从位置 A 移动到位置 B”。React 的职责是根据状态计算 UI,提交 DOM 更新;真正的时间插值通常由 CSS、浏览器的 View Transitions 能力或第三方动画库完成。
因此,React 动画首先要区分三件事:
- 状态变化:例如
open从false变成true。 - DOM 生命周期:元素是继续存在但改变样式,还是被卸载。
- 视觉插值:浏览器或动画库如何在一段时间内计算中间帧。
如果只改变 display: none,没有可插值的中间状态;如果元素已经卸载,CSS 也没有机会播放退出动画。大多数动画问题都可以追溯到这两个条件没有被满足。
一、动画的基本模型:状态、提交和插值
设组件状态为 ,React 根据状态生成 UI:
例如:
const [open, setOpen] = useState(false);
return (
<section className={open ? "panel panel-open" : "panel"}>
内容
</section>
);
当调用 setOpen(true) 时,过程可以简化为:
- React 更新状态;
- React 重新执行组件;
- 得到新的
className; - React 将差异提交到 DOM;
- 浏览器根据新旧 CSS 样式计算布局、绘制和合成;
- 如果属性配置了
transition或animation,浏览器生成中间帧。
这里的关键是:React 只负责第 1~4 步,CSS 动画发生在 React 提交之后。
动画可以表示为从起始值 到目标值 的插值:
其中:
- 是归一化时间;
- 是缓动函数,例如
ease-out; - 是某一时刻的属性值。
并不是所有 CSS 属性都能这样插值。opacity、transform 通常适合动画;display: none 是离散状态,不能自然地在 none 和 block 之间产生连续值。
二、CSS Transition:最简单、最可靠的 React 动画
2.1 transition 只描述“变化过程”
下面的组件通过 React 状态控制面板是否展开:
import { useState } from "react";
import "./Panel.css";
export function PanelExample() {
const [open, setOpen] = useState(false);
return (
<div>
<button
type="button"
aria-expanded={open}
aria-controls="settings-panel"
onClick={() => setOpen((value) => !value)}
>
{open ? "收起设置" : "展开设置"}
</button>
<section
id="settings-panel"
className={`panel ${open ? "panel--open" : ""}`}
>
<div className="panel__content">
设置内容
</div>
</section>
</div>
);
}
.panel {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transform: translateY(-0.25rem);
transition:
grid-template-rows 220ms ease,
opacity 160ms ease,
transform 220ms ease;
}
.panel__content {
min-height: 0;
overflow: hidden;
}
.panel--open {
grid-template-rows: 1fr;
opacity: 1;
transform: translateY(0);
}
这里没有让 React 逐帧更新状态。React 只切换 panel--open,浏览器负责计算动画。
grid-template-rows: 0fr 到 1fr 是一种常见的“内容高度展开”技巧:
0fr使内部轨道没有可用空间;1fr让轨道占据可用空间;min-height: 0防止内容自身的最小高度阻止收缩;overflow: hidden隐藏展开区域之外的内容。
它避免了直接动画 height: auto 的问题。传统 CSS 中,auto 通常不是一个可以直接插值的数值。若内容高度由文字、图片和响应式布局共同决定,硬编码 height: 300px 会产生截断或空白。
2.2 transition 的生命周期
.card {
transition:
opacity 200ms ease-out,
transform 200ms ease-out;
}
.card:hover {
opacity: 0.8;
transform: translateY(-4px);
}
这段规则的含义是:
- 当
opacity或transform的计算值发生变化时,启动过渡; - 过渡持续
200ms; - 使用
ease-out计算速度曲线; - 属性回到原值时,也会按同样规则反向过渡。
transition 本身不会触发变化。下面的代码没有效果:
.card {
transition: transform 200ms ease;
}
如果没有 :hover、类名、内联样式或其他状态改变 transform,就没有动画可播放。
2.3 进入和退出动画要求元素仍在 DOM 中
以下写法不能播放退出动画:
return open ? <div className="dialog dialog--open">内容</div> : null;
当 open 变成 false 时,React 直接删除节点。浏览器收到的是“节点消失”,而不是“节点仍存在但 opacity 从 1 变成 0”。
最小的 CSS 退出动画写法是让节点保持挂载,再用状态控制视觉效果:
type ToastProps = {
visible: boolean;
onHidden: () => void;
};
export function Toast({ visible, onHidden }: ToastProps) {
return (
<div
className={`toast ${visible ? "toast--visible" : "toast--hidden"}`}
onTransitionEnd={(event) => {
if (
event.propertyName === "opacity" &&
!visible
) {
onHidden();
}
}}
aria-hidden={!visible}
>
保存成功
</div>
);
}
.toast {
pointer-events: none;
opacity: 0;
transform: translateY(8px);
transition:
opacity 180ms ease,
transform 180ms ease;
}
.toast--visible {
pointer-events: auto;
opacity: 1;
transform: translateY(0);
}
.toast--hidden {
visibility: hidden;
}
真实应用中还要考虑两个边界:
transitionend可能因为用户切换页面、动画被覆盖或prefers-reduced-motion而不按预期触发;- 过渡涉及多个属性时,事件可能触发多次,因此应检查
propertyName。
如果退出后最终需要从 DOM 删除,就需要额外的状态,例如:
closed -> entering -> open -> exiting -> closed
其中 closed 才真正卸载节点。手动维护这套状态容易出现快速重复点击、组件卸载和事件竞态,第三方库通常专门处理这些问题。
三、使用哪些 CSS 属性:布局属性与合成属性
动画性能不能简单归结为“某个属性永远好,另一个属性永远坏”,但可以先理解浏览器工作的几个阶段:
- 样式计算:确定最终 CSS;
- 布局(layout/reflow):计算元素几何位置和尺寸;
- 绘制(paint):生成像素内容;
- 合成(composite):把图层组合到屏幕。
transform 和 opacity 在许多浏览器实现中可以主要由合成阶段处理,因此常用于移动、缩放和淡入淡出:
.item {
opacity: 0;
transform: translateY(12px) scale(0.98);
transition:
opacity 180ms ease,
transform 180ms ease;
}
.item--shown {
opacity: 1;
transform: translateY(0) scale(1);
}
但是,“使用 transform 就一定不卡”并不成立:
- 元素内容过大仍可能造成绘制压力;
- 频繁创建合成层可能增加显存消耗;
will-change不是免费优化,不能大量添加;- 动画期间如果同时触发布局读取和写入,仍会造成性能问题。
例如,不应在每一帧都通过 React 状态更新来移动元素:
// 不推荐:每帧 setState,导致组件树持续重新渲染
useEffect(() => {
let frame = 0;
const tick = () => {
setX((value) => value + 1);
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, []);
如果确实需要逐帧控制,应优先直接操作动画库的值、CSS 自定义属性或 DOM,而不是让整个 React 树参与每一帧更新。React 状态更适合表示“动画开始、结束或当前业务阶段”,不适合承载高频的视觉时钟。
四、React 的 Transition:调度优先级,不是视觉过渡
React 的 useTransition 名称容易与 CSS Transition 混淆,但两者解决的问题不同。
import { useState, useTransition } from "react";
export function SearchResults() {
const [query, setQuery] = useState("");
const [deferredQuery, setDeferredQuery] = useState("");
const [isPending, startTransition] = useTransition();
function handleChange(nextQuery: string) {
setQuery(nextQuery);
startTransition(() => {
setDeferredQuery(nextQuery);
});
}
return (
<section>
<label>
搜索
<input
value={query}
onChange={(event) => handleChange(event.target.value)}
/>
</label>
{isPending && <p role="status">正在更新结果……</p>}
<SearchResultList query={deferredQuery} />
</section>
);
}
function SearchResultList({ query }: { query: string }) {
return <p>结果:{query || "全部"}</p>;
}
这里的 startTransition 表示:deferredQuery 相关的更新优先级较低,在用户输入等紧急更新之后处理。它不会:
- 自动增加
opacity; - 自动移动元素;
- 自动播放进入或退出动画;
- 把一次 React 更新变成 CSS transition。
isPending 只是一个 React 状态,可以被用来显示加载指示器:
{isPending && <span className="spinner" aria-label="加载中" />}
但 spinner 的旋转仍由 CSS 或动画库完成。
4.1 为什么并发调度与动画可以组合,但不能互相替代
一个列表切换过程可能包含两条独立链路:
用户输入
├─ 紧急更新:输入框的 value 立即变化
└─ Transition 更新:昂贵的结果列表稍后渲染
↓
DOM 发生变化
↓
CSS/Motion 播放视觉动画
React Transition 决定“何时让这次渲染参与竞争”;CSS 或 Motion 决定“DOM 变更后如何呈现”。
如果把 startTransition 当作动画 API,常见失败表现是:代码没有报错,但界面只是延迟更新,完全没有过渡效果。
4.2 useTransition 的边界
Transition 更新不是事务锁,也不保证每个中间状态都被用户看到。React 可以合并、打断并重新执行低优先级渲染。如果渲染过程中触发副作用,可能导致重复或错误行为;渲染函数应保持纯粹,网络请求、订阅和 DOM 副作用应放在合适的 Effect 或数据层中。
此外,startTransition 不会自动包住异步函数中稍后发生的所有更新。跨越 await 后,如果需要把后续状态更新视为 Transition,应该根据当前 React 版本的 API 语义显式处理,而不能假设整个异步流程天然属于同一个 Transition。
React 19 的 Actions、useActionState 等能力可以帮助管理异步提交状态,但它们仍然不是视觉动画 API。加载、提交、错误和成功状态可以驱动 CSS 或 Motion,而不是被这些 API 自动动画化。
五、Motion:用状态声明进入、退出和布局变化
这里的 Motion 指现代 Motion 库提供的 React 动画能力,通常通过 motion/react 导入。它不是 React 内置 API。使用前需要安装对应包:
npm install motion
一个基本的进入动画如下:
import { motion } from "motion/react";
export function WelcomeCard() {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
欢迎回来
</motion.div>
);
}
这些属性的含义是:
initial:首次出现时的初始值;animate:组件存在时的目标值;transition:时间、缓动和其他动画参数;y通常映射为transform: translateY(...)。
与手写 CSS 相比,Motion 的优势不是“所有动画都更快”,而是把复杂的动画状态和生命周期封装成声明式组件,尤其适合:
- 组件挂载时进入;
- 组件卸载前退出;
- 列表项增删;
- 共享布局变化;
- 手势和拖拽;
- 多个动画值之间的协调。
5.1 退出动画与 AnimatePresence
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";
export function ModalExample() {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
打开
</button>
<AnimatePresence>
{open && (
<motion.div
key="modal"
className="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.18 }}
>
<div className="modal__body">
<p>对话框内容</p>
<button type="button" onClick={() => setOpen(false)}>
关闭
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}
.modal {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgb(0 0 0 / 35%);
}
.modal__body {
padding: 1rem;
background: white;
border-radius: 0.5rem;
}
关键机制是:
open为true,React 返回motion.div;- Motion 播放
initial -> animate; open为false,React 的条件表达式不再返回该节点;AnimatePresence拦截这次移除;- Motion 先播放
exit; - 退出动画完成后,节点才真正从 DOM 移除。
这正好解决了手写 CSS 中“退出节点已经不存在”的生命周期问题。
key 也非常重要。对于列表或模式切换,React 通过 key 判断是不是同一个元素。如果错误地复用同一个 key,Motion 可能不会得到预期的进入和退出边界。
5.2 Motion 与 React 服务端渲染
在 Next.js 等框架中,使用事件、状态和 Motion 组件的文件通常需要客户端边界:
"use client";
import { motion } from "motion/react";
export function AnimatedButton() {
return (
<motion.button whileTap={{ scale: 0.96 }}>
提交
</motion.button>
);
}
"use client" 的含义是:该模块及其客户端依赖从客户端组件边界开始执行,能够使用事件处理器和浏览器能力。它不是“让服务端完全不渲染 HTML”,也不是动画本身的开关。
服务端渲染时应保证初始结构和初始属性可预测:
- 不要在渲染阶段直接读取
window、document; - 不要根据服务端不存在的随机值决定
initial; - 需要浏览器媒体查询时,用客户端 Effect 或库提供的安全机制同步;
- 不要为了动画把整个页面都改成客户端组件。
否则可能出现 hydration mismatch,表现为控制台警告、初始闪烁或动画从错误的起点开始。
六、布局动画:位置变化与尺寸变化的真正难点
布局动画指元素的几何关系发生变化时,对位置、尺寸或布局重排进行平滑过渡。例如:
- 网格从三列变为两列;
- 列表删除一项,其他项向上移动;
- 卡片内容展开导致下面的元素下移;
- 同一个选中指示器从一个标签移动到另一个标签。
直接给 top、left、width 或 height 加 CSS transition 有时可以工作,但复杂布局通常需要先得到变化前后的几何信息。典型的 FLIP 思路是:
- First:记录变化前的矩形;
- Last:应用新布局并记录变化后的矩形;
- Invert:通过反向
transform把元素视觉上放回旧位置; - Play:把反向变换过渡到
none。
假设旧位置是 ,新位置是 ,动画开始时可设置:
这样虽然 DOM 已经位于新位置,视觉上却暂时回到旧位置;随后将 transform 过渡到 translateX(0),用户就看到它移动到新位置。
手动实现 FLIP 会遇到测量时机、滚动容器、元素删除和 React 提交顺序等问题。Motion 的 layout 属性可以处理常见布局变化:
import { useState } from "react";
import { motion } from "motion/react";
const items = ["A", "B", "C"];
export function LayoutExample() {
const [selected, setSelected] = useState("A");
return (
<div className="tabs">
{items.map((item) => (
<button
key={item}
type="button"
className="tab"
onClick={() => setSelected(item)}
>
{item}
{selected === item && (
<motion.span
layoutId="active-tab"
className="active-tab"
transition={{ type: "spring", bounce: 0.2 }}
/>
)}
</button>
))}
</div>
);
}
.tabs {
display: flex;
gap: 0.25rem;
}
.tab {
position: relative;
z-index: 0;
padding: 0.5rem 0.75rem;
border: 0;
background: transparent;
}
.active-tab {
position: absolute;
z-index: -1;
inset: 0;
border-radius: 0.375rem;
background: #dbeafe;
}
这里有两个不同概念:
layout:同一个组件的布局矩形发生变化时进行动画;layoutId:不同 React 位置中具有相同标识的元素被视为同一个视觉对象,从旧位置移动到新位置。
布局动画通常依赖测量 DOM 矩形,因此比单纯的 opacity 动画更容易受到以下因素影响:
- 父元素使用
overflow: hidden; - CSS
position和堆叠上下文改变; - 滚动容器不是
window; - 字体加载前后尺寸变化;
- 图片没有固定宽高导致布局跳动;
- 列表 key 不稳定;
- 动画过程中又发生一次布局更新。
6.1 CSS View Transitions 的位置
浏览器 View Transitions API 也可以对文档或 DOM 状态变化生成过渡效果。它的思路不是逐个元素手写 FLIP,而是让浏览器捕获新旧视图并进行过渡。它适合页面级或视图级切换,但兼容性、交互细节和框架集成仍需按目标浏览器验证。
React 的具体 View Transition 集成属于版本和发布通道敏感能力,不能把它与稳定的 useTransition 混为一谈:
useTransition:React 调度低优先级更新;- CSS View Transitions:浏览器视图过渡机制;
- Motion
layout:第三方库通过布局测量和动画实现布局变化。
选择哪一种取决于目标浏览器、应用路由、组件粒度和对交互控制的要求。
七、减少动态:prefers-reduced-motion 不只是“关闭动画”
减少动态是对有前庭障碍、晕动症、注意力敏感或其他无障碍需求用户提供低运动版本。浏览器通过媒体查询暴露用户偏好:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
这段规则可以快速降低全局动态,但它也可能过于粗糙。例如,某些动画承担状态可见性职责,直接删除后可能出现内容突然跳变、焦点位置不清楚或状态无法感知。
更好的方法是区分:
- 装饰性运动:可以删除;
- 状态反馈:保留淡入、颜色变化或即时反馈;
- 功能性运动:保留必要的空间关系,但缩短时间、减少位移。
.drawer {
opacity: 0;
transform: translateX(100%);
transition:
transform 240ms ease,
opacity 240ms ease;
}
.drawer--open {
opacity: 1;
transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
.drawer {
transition-duration: 1ms;
transform: none;
}
.drawer--open {
opacity: 1;
}
}
7.1 Motion 中读取减少动态偏好
Motion 可以通过 useReducedMotion 读取用户偏好:
import { motion, useReducedMotion } from "motion/react";
export function ReducedMotionCard() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={
shouldReduceMotion
? { duration: 0.01 }
: { duration: 0.2, ease: "easeOut" }
}
>
内容
</motion.div>
);
}
这里需要意识到媒体查询结果可能在服务端不可知。服务端首先输出一个初始版本,客户端挂载后才能读取浏览器偏好。若初始动画配置与客户端配置差异过大,可能出现水合后的闪烁。因此,生产代码通常同时保留 CSS 级保护,并让 JS 级配置尽量具有稳定的初始结构。
不要仅通过 aria-hidden 隐藏仍可交互的动画元素。视觉隐藏、可访问性树隐藏和键盘焦点管理是不同问题:
opacity: 0的元素仍可能可聚焦;visibility: hidden通常会影响可见性和交互;display: none会从布局和可访问性树中移除;- 对话框关闭时应同步处理焦点返回和背景内容的交互权限。
八、CSS、React Transition 和 Motion 如何选择
可以用“变化的来源”和“生命周期复杂度”判断:
| 场景 | 适合方案 | 原因 |
|---|---|---|
| hover、focus、颜色、透明度 | CSS Transition | 不需要 React 逐帧参与 |
| 简单展开/收起 | CSS Transition | 状态只切换类名 |
| 组件挂载和卸载 | Motion 或专门的 Transition 组件 | 需要延迟卸载 |
| 输入响应和昂贵列表渲染 | React useTransition |
解决更新优先级,不负责视觉 |
| 列表重排、共享元素、拖拽 | Motion 布局能力 | 需要测量和协调几何变化 |
| 页面级路由切换 | 框架能力或 View Transitions | 需要跨视图管理 |
| 无障碍适配 | CSS 媒体查询 + JS 偏好 | 同时覆盖样式和动画库 |
一个常见误区是:所有动画都使用 Motion。这样会增加客户端 JavaScript、依赖体积和运行时复杂度;对于按钮 hover,CSS 已经足够。相反,另一个误区是:所有动画都手写 CSS。对于退出卸载、共享布局和复杂列表,这会把生命周期管理分散到多个事件处理器中,维护成本更高。
九、常见失败表现和诊断方法
9.1 动画完全不播放
按以下顺序检查:
- React 状态是否真的改变;
- 新旧元素是否是同一个 DOM 节点;
- 新旧 CSS 计算值是否不同;
transition是否写在变化前后都存在的基础类上;- 属性是否为可插值属性;
- 是否被
prefers-reduced-motion、display: none或其他规则覆盖。
浏览器开发者工具中的 Computed 面板可以确认最终的 transition-property、transition-duration 和目标属性值,而不是只看源码。
9.2 进入动画第一次渲染就没有起点
如果服务端输出的 HTML 已经是最终可见状态,客户端挂载后才添加动画类,浏览器可能没有观察到一次有效的起始值变化。Motion 的 initial 可以显式声明起点;手写 CSS 则应确保初始类和目标类之间存在可观察的提交变化。
9.3 退出动画被截断
通常是条件渲染直接返回 null。解决路径有两类:
- 保持节点挂载,动画完成后再清理;
- 使用
AnimatePresence等管理退出生命周期的组件。
还要检查父节点是否在子动画完成前被卸载,以及是否存在 overflow: hidden 截断移动区域。
9.4 布局动画跳跃
优先检查:
- 列表是否使用稳定且唯一的
key; - 是否在动画中改变了
display、定位方式或父级布局; - 图片和字体尺寸是否在动画期间才确定;
- 是否在同一事件中同时修改了多个互相依赖的状态;
- 是否误以为
useTransition会产生视觉过渡。
9.5 动画期间界面变得更卡
可能原因不是动画属性本身,而是每一帧触发了 React 大范围重渲染、强制布局或大型绘制。可以在 Performance 面板查看是否出现连续的 Recalculate Style、Layout、Paint;在 React DevTools Profiler 中确认是否有无关组件反复渲染。优化方向应根据证据选择,而不是无条件添加 will-change 或把所有内容移入动画库。
十、一个完整的决策示例
假设要实现一个搜索结果页面:
- 输入框必须立即响应;
- 结果计算较昂贵;
- 结果列表切换时希望淡入;
- 删除结果时希望播放退出动画;
- 用户设置了减少动态时,不应出现明显位移。
可以拆成四层:
"use client";
import { useState, useTransition } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
export function SearchPage() {
const [query, setQuery] = useState("");
const [resultQuery, setResultQuery] = useState("");
const [isPending, startTransition] = useTransition();
const reduceMotion = useReducedMotion();
function onQueryChange(value: string) {
setQuery(value);
startTransition(() => {
setResultQuery(value);
});
}
const results = getResults(resultQuery);
return (
<main>
<input
value={query}
onChange={(event) => onQueryChange(event.target.value)}
aria-label="搜索"
/>
{isPending && <span role="status">更新中</span>}
<AnimatePresence mode="popLayout">
{results.map((result) => (
<motion.article
key={result.id}
layout
initial={
reduceMotion
? { opacity: 0 }
: { opacity: 0, y: 12 }
}
animate={{ opacity: 1, y: 0 }}
exit={
reduceMotion
? { opacity: 0 }
: { opacity: 0, y: -8 }
}
transition={
reduceMotion
? { duration: 0.01 }
: { duration: 0.18 }
}
>
{result.title}
</motion.article>
))}
</AnimatePresence>
</main>
);
}
type Result = {
id: string;
title: string;
};
function getResults(query: string): Result[] {
const all: Result[] = [
{ id: "1", title: "React 基础" },
{ id: "2", title: "CSS 动画" },
];
return query
? all.filter((item) => item.title.includes(query))
: all;
}
这段代码的因果关系是:
query是紧急状态,保证输入框立即更新;resultQuery放入 React Transition,允许昂贵结果渲染让出优先级;AnimatePresence保证离开的结果仍在 DOM 中,直到exit完成;layout处理其他结果因为删除而发生的位置变化;useReducedMotion减少位移,但仍保留很短的透明度反馈;key={result.id}让 React 和 Motion 正确识别结果实体。
这个示例仍有边界:真实搜索通常涉及异步请求、请求取消、错误状态和服务端数据缓存。网络请求的竞态不能靠动画解决;应使用请求库或显式的请求序列号、AbortController 等机制保证旧响应不会覆盖新查询结果。动画只负责结果已经确定后如何呈现。
结语
React 动画的核心不是“在组件里不断修改坐标”,而是把职责分开:
- React 状态描述业务事实;
- React
useTransition调整渲染更新的优先级; - CSS Transition 处理简单、局部且持续挂载的样式变化;
- Motion 处理进入、退出、手势和布局测量;
- 浏览器或框架级 View Transitions 适合更大范围的视图切换;
prefers-reduced-motion决定动画是否应降低动态程度。
当动画失败时,先问三个问题:状态是否变化、节点是否仍存在、变化的属性是否可插值。再根据是否涉及卸载、布局测量和并发渲染选择工具,通常比盲目增加动画代码更容易得到正确结果。
系列导航与关联阅读
- 系列入口:React 完整学习路线:从渲染与 Hooks 到服务端组件和生产架构
- 上一篇:React 样式工程:CSS Modules、CSS-in-JS、原子 CSS 和主题
- 下一篇:React 与 Web Components:属性、事件、Ref、类型和互操作
官方资料
本文依据 React 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论