mastering-animate-presence
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMastering AnimatePresence
精通AnimatePresence
Review Motion code for AnimatePresence and exit animation best practices.
评审Motion代码,检查AnimatePresence和退出动画的最佳实践。
How It Works
工作原理
- Read the specified files (or prompt user for files/pattern)
- Check against all rules below
- Output findings in format
file:line
- 读取指定文件(或提示用户输入文件/匹配模式)
- 对照以下所有规则进行检查
- 以格式输出检查结果
file:line
Rule Categories
规则分类
| Priority | Category | Prefix |
|---|---|---|
| 1 | Exit Animations | |
| 2 | Presence Hooks | |
| 3 | Mode Selection | |
| 4 | Nested Exits | |
| 优先级 | 分类 | 前缀 |
|---|---|---|
| 1 | 退出动画 | |
| 2 | 存在状态钩子 | |
| 3 | 模式选择 | |
| 4 | 嵌套退出 | |
Rules
规则
Exit Animation Rules
退出动画规则
exit-requires-wrapper
exit-requires-wrapperexit-requires-wrapper
exit-requires-wrapperConditional motion elements must be wrapped in AnimatePresence.
Fail:
tsx
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}Pass:
tsx
<AnimatePresence>
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
</AnimatePresence>条件渲染的motion元素必须包裹在AnimatePresence中。
错误示例:
tsx
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}正确示例:
tsx
<AnimatePresence>
{isVisible && (
<motion.div exit={{ opacity: 0 }} />
)}
</AnimatePresence>exit-prop-required
exit-prop-requiredexit-prop-required
exit-prop-requiredElements inside AnimatePresence should have exit prop defined.
Fail:
tsx
<AnimatePresence>
{isOpen && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
)}
</AnimatePresence>Pass:
tsx
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>AnimatePresence内部的元素必须定义exit属性。
错误示例:
tsx
<AnimatePresence>
{isOpen && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
)}
</AnimatePresence>正确示例:
tsx
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>exit-key-required
exit-key-requiredexit-key-required
exit-key-requiredDynamic lists inside AnimatePresence must have unique keys.
Fail:
tsx
<AnimatePresence>
{items.map((item, index) => (
<motion.div key={index} exit={{ opacity: 0 }} />
))}
</AnimatePresence>Pass:
tsx
<AnimatePresence>
{items.map((item) => (
<motion.div key={item.id} exit={{ opacity: 0 }} />
))}
</AnimatePresence>AnimatePresence内部的动态列表必须设置唯一key。
错误示例:
tsx
<AnimatePresence>
{items.map((item, index) => (
<motion.div key={index} exit={{ opacity: 0 }} />
))}
</AnimatePresence>正确示例:
tsx
<AnimatePresence>
{items.map((item) => (
<motion.div key={item.id} exit={{ opacity: 0 }} />
))}
</AnimatePresence>exit-matches-initial
exit-matches-initialexit-matches-initial
exit-matches-initialExit animation should mirror initial for symmetry.
Fail:
tsx
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ scale: 0 }}
/>Pass:
tsx
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
/>退出动画应与初始动画对称。
错误示例:
tsx
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ scale: 0 }}
/>正确示例:
tsx
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
/>Presence Hook Rules
存在状态钩子规则
presence-hook-in-child
presence-hook-in-childpresence-hook-in-child
presence-hook-in-childuseIsPresent must be called from child of AnimatePresence, not parent.
Fail:
tsx
function Parent() {
const isPresent = useIsPresent(); // Wrong location
return (
<AnimatePresence>
{show && <Child />}
</AnimatePresence>
);
}Pass:
tsx
function Child() {
const isPresent = useIsPresent(); // Correct location
return <motion.div data-exiting={!isPresent} />;
}useIsPresent必须在AnimatePresence的子组件中调用,而非父组件。
错误示例:
tsx
function Parent() {
const isPresent = useIsPresent(); // 位置错误
return (
<AnimatePresence>
{show && <Child />}
</AnimatePresence>
);
}正确示例:
tsx
function Child() {
const isPresent = useIsPresent(); // 位置正确
return <motion.div data-exiting={!isPresent} />;
}presence-safe-to-remove
presence-safe-to-removepresence-safe-to-remove
presence-safe-to-removeWhen using usePresence, always call safeToRemove after async work.
Fail:
tsx
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup(); // Never calls safeToRemove
}
}, [isPresent]);
}Pass:
tsx
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup().then(safeToRemove);
}
}, [isPresent, safeToRemove]);
}使用usePresence时,异步操作完成后必须调用safeToRemove。
错误示例:
tsx
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup(); // 从未调用safeToRemove
}
}, [isPresent]);
}正确示例:
tsx
function AsyncComponent() {
const [isPresent, safeToRemove] = usePresence();
useEffect(() => {
if (!isPresent) {
cleanup().then(safeToRemove);
}
}, [isPresent, safeToRemove]);
}presence-disable-interactions
presence-disable-interactionspresence-disable-interactions
presence-disable-interactionsDisable interactions on exiting elements using isPresent.
Fail:
tsx
function Card() {
const isPresent = useIsPresent();
return <button onClick={handleClick}>Click</button>;
// Button clickable during exit
}Pass:
tsx
function Card() {
const isPresent = useIsPresent();
return (
<button onClick={handleClick} disabled={!isPresent}>
Click
</button>
);
}使用isPresent禁用退出元素的交互。
错误示例:
tsx
function Card() {
const isPresent = useIsPresent();
return <button onClick={handleClick}>Click</button>;
// 按钮在退出期间仍可点击
}正确示例:
tsx
function Card() {
const isPresent = useIsPresent();
return (
<button onClick={handleClick} disabled={!isPresent}>
Click
</button>
);
}Mode Selection Rules
模式选择规则
mode-wait-doubles-duration
mode-wait-doubles-durationmode-wait-doubles-duration
mode-wait-doubles-durationMode "wait" nearly doubles animation duration; adjust timing accordingly.
Fail:
tsx
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.3 }} />
</AnimatePresence>
// Total time: ~600ms (too slow)Pass:
tsx
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.15 }} />
</AnimatePresence>
// Total time: ~300ms (acceptable)"wait"模式几乎会使动画时长翻倍;需相应调整计时。
错误示例:
tsx
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.3 }} />
</AnimatePresence>
// 总时长:~600ms(过慢)正确示例:
tsx
<AnimatePresence mode="wait">
<motion.div transition={{ duration: 0.15 }} />
</AnimatePresence>
// 总时长:~300ms(可接受)mode-sync-layout-conflict
mode-sync-layout-conflictmode-sync-layout-conflict
mode-sync-layout-conflictMode "sync" causes layout conflicts; position exiting elements absolutely.
Fail:
tsx
<AnimatePresence mode="sync">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>
// Exiting and entering elements compete for spacePass:
tsx
<AnimatePresence mode="popLayout">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>"sync"模式会导致布局冲突;需将退出元素设置为绝对定位。
错误示例:
tsx
<AnimatePresence mode="sync">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>
// 退出和进入元素争夺空间正确示例:
tsx
<AnimatePresence mode="popLayout">
{items.map(item => (
<motion.div exit={{ opacity: 0 }}>{item}</motion.div>
))}
</AnimatePresence>mode-pop-layout-for-lists
mode-pop-layout-for-listsmode-pop-layout-for-lists
mode-pop-layout-for-listsUse popLayout mode for list reordering animations.
Fail:
tsx
<AnimatePresence>
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// Layout shifts during exitPass:
tsx
<AnimatePresence mode="popLayout">
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>列表重排动画需使用popLayout模式。
错误示例:
tsx
<AnimatePresence>
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// 退出期间布局偏移正确示例:
tsx
<AnimatePresence mode="popLayout">
{items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>Nested Exit Rules
嵌套退出规则
nested-propagate-required
nested-propagate-requirednested-propagate-required
nested-propagate-requiredNested AnimatePresence must use propagate prop for coordinated exits.
Fail:
tsx
<AnimatePresence>
{isOpen && (
<motion.div exit={{ opacity: 0 }}>
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id} exit={{ scale: 0 }} />
))}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
// Children vanish instantly when parent exitsPass:
tsx
<AnimatePresence propagate>
{isOpen && (
<motion.div exit={{ opacity: 0 }}>
<AnimatePresence propagate>
{items.map(item => (
<motion.div key={item.id} exit={{ scale: 0 }} />
))}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>嵌套的AnimatePresence必须使用propagate属性以实现协同退出。
错误示例:
tsx
<AnimatePresence>
{isOpen && (
<motion.div exit={{ opacity: 0 }}>
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id} exit={{ scale: 0 }} />
))}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
// 父元素退出时子元素立即消失正确示例:
tsx
<AnimatePresence propagate>
{isOpen && (
<motion.div exit={{ opacity: 0 }}>
<AnimatePresence propagate>
{items.map(item => (
<motion.div key={item.id} exit={{ scale: 0 }} />
))}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>nested-consistent-timing
nested-consistent-timingnested-consistent-timing
nested-consistent-timingParent and child exit durations should be coordinated.
Fail:
tsx
// Parent exits in 100ms, children in 500ms
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.1 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.5 }} />
</motion.div>Pass:
tsx
// Parent waits for children or exits simultaneously
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.15 }} />
</motion.div>父元素和子元素的退出时长需保持协调。
错误示例:
tsx
// 父元素100ms退出,子元素500ms退出
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.1 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.5 }} />
</motion.div>正确示例:
tsx
// 父元素等待子元素完成或同时退出
<motion.div exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
<motion.div exit={{ scale: 0 }} transition={{ duration: 0.15 }} />
</motion.div>Output Format
输出格式
When reviewing files, output findings as:
file:line - [rule-id] description of issue
Example:
components/modal/index.tsx:23 - [exit-requires-wrapper] Conditional motion.div not wrapped in AnimatePresence
components/modal/index.tsx:45 - [exit-prop-required] Missing exit prop on motion element评审文件时,输出结果格式如下:
file:line - [规则ID] 问题描述
示例:
components/modal/index.tsx:23 - [exit-requires-wrapper] 条件渲染的motion.div未包裹在AnimatePresence中
components/modal/index.tsx:45 - [exit-prop-required] motion元素缺少exit属性Summary Table
汇总表格
After findings, output a summary:
| Rule | Count | Severity |
|---|---|---|
| 2 | HIGH |
| 3 | HIGH |
| 1 | MEDIUM |
输出检查结果后,需输出汇总信息:
| 规则 | 数量 | 严重程度 |
|---|---|---|
| 2 | 高 |
| 3 | 高 |
| 1 | 中 |