mcpField
M
mastering-animate-presence

mastering-animate-presence

github.com/raphaelsalaja设计与 UI46326 次安装今天更新
代码仓库

关于 mastering-animate-presence

用于审查 Motion 中 AnimatePresence 用法的代码审查技能。它按退出动画、presence 钩子、模式选择和嵌套退出规则检查指定文件,并按文件与行号报告问题。

主要功能

  1. 01检查 AnimatePresence 退出动画模式
  2. 02审查 presence 钩子用法
  3. 03检查模式选择与时长
  4. 04审查嵌套退出协调
  5. 05按规则编号和行号报告问题

适用场景

  1. 01审查带条件退出的 Motion 组件
  2. 02检查列表键和 exit 属性
  3. 03审计异步清理中的 presence 钩子
  4. 04检查嵌套退出和动画模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/raphaelsalaja/skills --skill mastering-animate-presence

安装后在 Claude Code 中输入/mastering-animate-presence确认已加载。

详细介绍

掌握 AnimatePresence

检查 Motion 代码,了解 AnimatePresence 和退出动画的最佳实践。

工作原理

  1. 读取指定文件(或提示用户提供文件/模式)
  2. 根据以下所有规则进行检查
  3. 以 file:line 格式输出检查结果

规则类别

优先级类别前缀
1退出动画exit-
2存在状态钩子presence-
3模式选择mode-
4嵌套退出nested-

规则

退出动画规则
exit-requires-wrapper

条件渲染的 motion 元素必须包裹在 AnimatePresence 中。

失败:

{isVisible && (
  <motion.div exit={{ opacity: 0 }} />
)}

通过:

<AnimatePresence>
  {isVisible && (
    <motion.div exit={{ opacity: 0 }} />
  )}
</AnimatePresence>
exit-prop-required

AnimatePresence 中的元素应定义 exit 属性。

失败:

<AnimatePresence>
  {isOpen && (
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
  )}
</AnimatePresence>

通过:

<AnimatePresence>
  {isOpen && (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  )}
</AnimatePresence>
exit-key-required

AnimatePresence 中的动态列表必须具有唯一的 key。

失败:

<AnimatePresence>
  {items.map((item, index) => (
    <motion.div key={index} exit={{ opacity: 0 }} />
  ))}
</AnimatePresence>

通过:

<AnimatePresence>
  {items.map((item) => (
    <motion.div key={item.id} exit={{ opacity: 0 }} />
  ))}
</AnimatePresence>
exit-matches-initial

退出动画应与初始动画相对应,以保持对称。

失败:

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ scale: 0 }}
/>

通过:

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: 20 }}
/>
存在状态钩子规则
presence-hook-in-child

必须在 AnimatePresence 的子组件中调用 useIsPresent,而不是在父组件中调用。

失败:

function Parent() {
  const isPresent = useIsPresent(); // Wrong location
  return (
    <AnimatePresence>
      {show && <Child />}
    </AnimatePresence>
  );
}

通过:

function Child() {
  const isPresent = useIsPresent(); // Correct location
  return <motion.div data-exiting={!isPresent} />;
}
presence-safe-to-remove

使用 usePresence 时,异步工作完成后务必调用 safeToRemove。

失败:

function AsyncComponent() {
  const [isPresent, safeToRemove] = usePresence();

  useEffect(() => {
    if (!isPresent) {
      cleanup(); // Never calls safeToRemove
    }
  }, [isPresent]);
}

通过:

function AsyncComponent() {
  const [isPresent, safeToRemove] = usePresence();

  useEffect(() => {
    if (!isPresent) {
      cleanup().then(safeToRemove);
    }
  }, [isPresent, safeToRemove]);
}
presence-disable-interactions

使用 isPresent 禁用退出元素的交互。

失败:

function Card() {
  const isPresent = useIsPresent();
  return <button onClick={handleClick}>Click</button>;
  // Button clickable during exit
}

通过:

function Card() {
  const isPresent = useIsPresent();
  return (
    <button onClick={handleClick} disabled={!isPresent}>
      Click
    </button>
  );
}
模式选择规则
mode-wait-doubles-duration

Mode "wait" 几乎会使动画时长翻倍;请相应调整时间设置。

失败:

<AnimatePresence mode="wait">
  <motion.div transition={{ duration: 0.3 }} />
</AnimatePresence>
// Total time: ~600ms (too slow)

通过:

<AnimatePresence mode="wait">
  <motion.div transition={{ duration: 0.15 }} />
</AnimatePresence>
// Total time: ~300ms (acceptable)
mode-sync-layout-conflict

Mode "sync" 会导致布局冲突;请将退出元素设为绝对定位。

失败:

<AnimatePresence mode="sync">
  {items.map(item => (
    <motion.div exit={{ opacity: 0 }}>{item}</motion.div>
  ))}
</AnimatePresence>
// Exiting and entering elements compete for space

通过:

<AnimatePresence mode="popLayout">
  {items.map(item => (
    <motion.div exit={{ opacity: 0 }}>{item}</motion.div>
  ))}
</AnimatePresence>
mode-pop-layout-for-lists

使用 popLayout mode 实现列表重排动画。

失败:

<AnimatePresence>
  {items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
// Layout shifts during exit

通过:

<AnimatePresence mode="popLayout">
  {items.map(item => <ListItem key={item.id} />)}
</AnimatePresence>
嵌套退出规则
nested-propagate-required

嵌套的 AnimatePresence 必须使用 propagate 属性,以协调退出动画。

失败:

<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

通过:

<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

父级和子级的退出时长应协调一致。

失败:

// 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>

通过:

// 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>

输出格式

审查文件时,按以下格式输出检查结果:

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

汇总表

输出检查结果后,提供汇总:

规则数量严重程度
exit-requires-wrapper2HIGH
exit-prop-required3HIGH
mode-wait-doubles-duration1MEDIUM

参考资料

评论

支持 Markdown