mcpField
M
mastering-animate-presence

mastering-animate-presence

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

关于 mastering-animate-presence

用于审查 Motion 代码中 AnimatePresence 和退出动画实践的 Agent Skill,面向编写或维护动画界面的开发者。它按规则检查代码,并以文件与行号列出问题及汇总。

主要功能

  1. 01检查 AnimatePresence 包装与 exit 属性
  2. 02检查列表键和退出动画对称性
  3. 03检查 Presence Hooks 的使用位置
  4. 04检查模式选择与布局冲突
  5. 05检查嵌套退出及交互处理
  6. 06按文件行号输出问题和统计

适用场景

  1. 01审查条件渲染的退出动画
  2. 02检查动态列表的动画设置
  3. 03定位 usePresence 异步清理问题
  4. 04评估嵌套动画的时序配置

安装

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

$npx skills add https://github.com/raphaelsalaja/skill --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