React组件挂载卸载动画实现及类切换失效解决方案
问题原因
你的代码存在两个核心逻辑问题,导致动画无法正常触发:
- 类名绑定逻辑错误:渲染逻辑中只有
pokemonDetails === true时才会给容器追加动画相关类,关闭流程中等待500ms后你直接将pokemonDetails设为false,容器会立刻移除所有样式(包括fadeOut),退场动画根本没有播放的机会。 - 入场动画缺少触发时机:打开组件时你同时设置
fadeIn类和切换pokemonDetails为true,React会合并这两次状态更新,浏览器首次渲染该DOM节点时会直接计算最终样式,不会产生从0高度到目标高度的变化过程,入场动画直接被跳过。
现有代码修复方法
你需要拆分状态,把「组件是否挂载到DOM」和「当前动画阶段」两个逻辑分开控制:
- 入场时先挂载DOM,等浏览器完成首次渲染后再添加入场动画类,触发样式变化产生动画
- 退场时先切换为退场动画类,等动画播放完成后再从DOM中移除组件
修复后的逻辑代码:
// 拆分两个独立状态,不要用一个布尔值同时控制挂载和动画 const [isMounted, setIsMounted] = useState(false); const [animationClass, setAnimationClass] = useState(""); const handlePokemonDetails = () => { if (isMounted) { // 退场流程:先切换退场动画类 setAnimationClass(styles.fadeOut); // 等待动画播放完成后再移除DOM setTimeout(() => { setIsMounted(false); setAnimationClass(""); }, 500); } else { // 入场流程:先把组件挂载到DOM setIsMounted(true); // 等待浏览器完成首帧渲染后再添加入场类,触发重排产生动画 requestAnimationFrame(() => { setAnimationClass(styles.fadeIn); }); } };
调整渲染逻辑,只要组件处于挂载状态就保留基础样式类:
{isMounted && ( <div className={`${styles.pokemonDetails} ${animationClass}`}></div> )}
最后给SASS动画补上forwards填充模式,避免动画播放结束后回到初始状态:
.fadeIn animation: fade 0.5s normal forwards .fadeOut animation: fade 0.5s reverse forwards
更可靠的生产级方案
手动维护状态同步动画时长很容易出现逻辑错漏,推荐使用生态内成熟的动画库减少重复代码:
轻量场景:react-transition-group
这是React官方维护的过渡动画基础库,专门处理组件挂载/卸载阶段的类名切换逻辑,你不需要手动写定时器等待动画完成。基础用法如下:import { CSSTransition } from 'react-transition-group'; // 组件渲染部分 <CSSTransition in={pokemonDetails} timeout={500} classNames="fade" unmountOnExit > <div className={styles.pokemonDetails}></div> </CSSTransition>对应SASS只需要遵循库的类名约定即可,不需要写@keyframes:
// 入场初始状态 .fade-enter { height: 0vmin; } // 入场动画执行阶段 .fade-enter-active { height: 39.5vmin; transition: height 0.5s; } // 退场初始状态 .fade-exit { height: 39.5vmin; } // 退场动画执行阶段 .fade-exit-active { height: 0vmin; transition: height 0.5s; }复杂动画场景:framer-motion
如果需要实现更复杂的组合动画、手势动画,可以使用framer-motion,通过声明式的API直接定义动画状态,不需要手动维护类名:import { motion, AnimatePresence } from "framer-motion"; // 组件渲染部分 <AnimatePresence> {pokemonDetails && ( <motion.div className={styles.pokemonDetails} initial={{ height: 0 }} animate={{ height: "39.5vmin" }} exit={{ height: 0 }} transition={{ duration: 0.5 }} /> )} </AnimatePresence>
内容的提问来源于stack exchange,提问作者Victor Piassi
相关产品推荐
相关产品推荐

