React组件内容变更时的顺序动画实现问题求助
实现React多行内容的顺序切换动画
核心思路
通过状态跟踪动画阶段 + CSS过渡严格控制三个动画的执行顺序:淡出旧内容 → 过渡容器高度 → 淡入新内容。利用transitionend事件监听每个动画的结束时机,触发下一阶段的动画逻辑,避免定时器带来的时序偏差。
自定义实现代码示例
以下是基于函数组件的完整实现,每行可直接替换为你的独立组件:
import { useState, useEffect, useRef } from 'react'; import './ContentSwitcher.css'; const CONTENT_ROW_HEIGHT = 40; // 固定行高,根据实际需求调整 export default function ContentSwitcher({ content }) { const [currentContent, setCurrentContent] = useState(content); const [nextContent, setNextContent] = useState(null); const [animationPhase, setAnimationPhase] = useState('idle'); // idle | fadingOut | resizing | fadingIn const [containerHeight, setContainerHeight] = useState(content.length * CONTENT_ROW_HEIGHT); const containerRef = useRef(null); // 监听内容变更,启动动画序列 useEffect(() => { if (content === currentContent) return; setNextContent(content); setAnimationPhase('fadingOut'); }, [content, currentContent]); // 监听淡出动画结束,触发高度过渡 useEffect(() => { if (animationPhase !== 'fadingOut') return; const handleTransitionEnd = (e) => { if (e.propertyName !== 'opacity') return; const newHeight = nextContent.length * CONTENT_ROW_HEIGHT; setContainerHeight(newHeight); setAnimationPhase('resizing'); }; const rows = containerRef.current.querySelectorAll('.content-row'); rows.forEach(row => row.addEventListener('transitionend', handleTransitionEnd)); return () => rows.forEach(row => row.removeEventListener('transitionend', handleTransitionEnd)); }, [animationPhase, nextContent]); // 监听高度过渡结束,切换内容并触发淡入 useEffect(() => { if (animationPhase !== 'resizing') return; const handleTransitionEnd = (e) => { if (e.propertyName !== 'height') return; setCurrentContent(nextContent); setNextContent(null); setAnimationPhase('fadingIn'); }; containerRef.current.addEventListener('transitionend', handleTransitionEnd); return () => containerRef.current.removeEventListener('transitionend', handleTransitionEnd); }, [animationPhase, nextContent]); // 监听淡入动画结束,回到空闲状态 useEffect(() => { if (animationPhase !== 'fadingIn') return; const handleTransitionEnd = (e) => { if (e.propertyName !== 'opacity') return; setAnimationPhase('idle'); }; const rows = containerRef.current.querySelectorAll('.content-row'); rows.forEach(row => row.addEventListener('transitionend', handleTransitionEnd)); return () => rows.forEach(row => row.removeEventListener('transitionend', handleTransitionEnd)); }, [animationPhase]); return ( <div ref={containerRef} className="content-container" style={{ height: `${containerHeight}px` }} > {(animationPhase === 'idle' || animationPhase === 'fadingIn') && ( currentContent.map((item, index) => ( <div key={index} className={`content-row ${animationPhase === 'fadingIn' ? 'fade-in' : ''}`} > {item} {/* 替换为你的独立行组件 */} </div> )) )} {animationPhase === 'fadingOut' && ( currentContent.map((item, index) => ( <div key={index} className="content-row fade-out" > {item} </div> )) )} </div> ); }
对应的CSS文件ContentSwitcher.css:
.content-container { overflow: hidden; transition: height 1s ease-in-out; } .content-row { height: 40px; /* 与JS中定义的固定行高保持一致 */ opacity: 1; transition: opacity 1s ease-in-out; } .content-row.fade-out { opacity: 0; } .content-row.fade-in { opacity: 0; animation: fadeIn 1s ease-in-out forwards; } @keyframes fadeIn { to { opacity: 1; } }
代码说明
- 状态管控:通过
animationPhase精准控制每个动画步骤,确保严格按顺序执行。 - 事件监听:用
transitionend替代定时器,保证动画完成后再触发下一步,避免视觉错乱。 - 高度计算:基于固定行高×行数动态计算容器高度,确保高度过渡平滑自然。
- 内容切换:在高度过渡完成后再替换内容,避免新旧内容同时显示的问题。
现成组件替代方案
如果不想手动实现,可以尝试扩展react-transition-group的序列动画能力,但自定义实现更灵活,能完全匹配你的三步动画需求,无需额外依赖。
内容的提问来源于stack exchange,提问作者Cesar Coll
相关产品推荐
相关产品推荐

