React条件淡出动画期间渲染旧值的问题与解决方案
React Transition Group 退出动画内容闪烁/Props为空的通用解决办法
问题场景
原本用条件渲染控制组件显示:
{valueToDisplay && <MyComponent {...valueToDisplay} />}
改成用CSSTransition实现淡出过渡后:
<CSSTransition in={!!valueToDisplay} unmountOnExit classNames="fade" addEndListener={(node, done) => node.addEventListener("transitionend", done, false)} > <MyComponent {...valueToDisplay} /> </CSSTransition>
出现了问题:当valueToDisplay变为null,in切换为false开始执行退出动画时,MyComponent的props会变成空值,导致组件内容瞬间变化(比如显示null),和淡出动画冲突,体验很差。
原因分析
React会立即响应valueToDisplay的变化,更新MyComponent的props,而CSSTransition的退出动画是异步执行的——组件内容已经变了,但动画还在跑,自然会出现闪烁。
通用解决方案:封装稳定过渡组件
不需要给每个子组件加缓存逻辑,也不用单独写特定的包装组件,我们可以封装一个通用的StableCSSTransition,内部自动缓存退出前的内容:
import { useRef, useEffect } from 'react'; import { CSSTransition } from 'react-transition-group'; const StableCSSTransition = ({ in: show, unmountOnExit, classNames, children, ...rest }) => { const cachedChildren = useRef(children); // 仅在组件处于显示状态时,更新缓存的内容 useEffect(() => { if (show) { cachedChildren.current = children; } }, [show, children]); return ( <CSSTransition in={show} unmountOnExit={unmountOnExit} classNames={classNames} {...rest} > {/* 显示时用最新内容,退出动画时用缓存的旧内容 */} {show ? children : cachedChildren.current} </CSSTransition> ); };
使用方式
直接替换原来的CSSTransition即可,用法完全一致:
<StableCSSTransition in={!!valueToDisplay} unmountOnExit classNames="fade" addEndListener={(node, done) => node.addEventListener("transitionend", done, false)} > <MyComponent {...valueToDisplay} /> </StableCSSTransition>
针对示例的优化
把示例中的Fade和FadeWithMemo替换成这个通用组件,就能实现退出动画时内容不变的效果:
import { useRef, useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { CSSTransition } from 'react-transition-group'; const StableCSSTransition = ({ in: show, unmountOnExit, classNames, children, ...rest }) => { const cachedChildren = useRef(children); useEffect(() => { if (show) { cachedChildren.current = children; } }, [show, children]); return ( <CSSTransition in={show} unmountOnExit={unmountOnExit} classNames={classNames} {...rest} > {show ? children : cachedChildren.current} </CSSTransition> ); }; const Pet = ({ type, age }) => { return ( <div> Your pet {type || 'null'} is age {age || 'null'} </div> ); }; const Example = () => { const [currentPet, setCurrentPet] = useState(null); const getPet = () => { return { type: (Math.random() > .5) ? 'Cat' : 'Dog', age: Math.floor(Math.random() * 15) + 1 }; }; return ( <> <button onClick={() => setCurrentPet(getPet())}>Set</button> <button onClick={() => setCurrentPet(null)}>Clear</button> <div> Fixed Version: <StableCSSTransition in={!!currentPet} unmountOnExit classNames="fade" addEndListener={(node, done) => node.addEventListener("transitionend", done, false)} > <Pet {...currentPet} /> </StableCSSTransition> </div> </> ); }; const root = createRoot(document.getElementById('root')); root.render(<Example />);
关于useMemo的疑问
useMemo主要用于缓存计算结果,依赖变化时会重新计算,不太适合这个场景——我们需要的是在show变为false时“冻结”之前的内容,而useRef的持久化特性更适合手动控制缓存时机,不会触发额外重渲染,逻辑也更清晰。
内容的提问来源于stack exchange,提问作者user715374
相关产品推荐
相关产品推荐

