You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 16:51:27