mastering-animate-presence

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Mastering AnimatePresence

精通AnimatePresence

Review Motion code for AnimatePresence and exit animation best practices.
评审Motion代码,检查AnimatePresence和退出动画的最佳实践。

How It Works

工作原理

  1. Read the specified files (or prompt user for files/pattern)
  2. Check against all rules below
  3. Output findings in
    file:line
    format
  1. 读取指定文件(或提示用户输入文件/匹配模式)
  2. 对照以下所有规则进行检查
  3. file:line
    格式输出检查结果

Rule Categories

规则分类

PriorityCategoryPrefix
1Exit Animations
exit-
2Presence Hooks
presence-
3Mode Selection
mode-
4Nested Exits
nested-
优先级分类前缀
1退出动画
exit-
2存在状态钩子
presence-
3模式选择
mode-
4嵌套退出
nested-

Rules

规则

Exit Animation Rules

退出动画规则

exit-requires-wrapper

exit-requires-wrapper

Conditional 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-required

Elements 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-required

Dynamic 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-initial

Exit 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-child

useIsPresent 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-remove

When 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-interactions

Disable 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-duration

Mode "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-conflict

Mode "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 space
Pass:
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-lists

Use popLayout mode for list reordering animations.
Fail:
tsx
<AnimatePresence>
  {items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// Layout shifts during exit
Pass:
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-required

Nested 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 exits
Pass:
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-timing

Parent 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:
RuleCountSeverity
exit-requires-wrapper
2HIGH
exit-prop-required
3HIGH
mode-wait-doubles-duration
1MEDIUM
输出检查结果后,需输出汇总信息:
规则数量严重程度
exit-requires-wrapper
2
exit-prop-required
3
mode-wait-doubles-duration
1

References

参考资料