React-transition-group: Transition enter animation doesn't work correctly with unmountOnExit

Created on 9 Jul 2021  路  11Comments  路  Source: reactjs/react-transition-group

What is the current behavior?

When using the Transition component with unmountOnExit, the exit animation works correctly, but the enter animation doesn't work. However, when I remove unmountOnExit, the enter animation works.

What is the expected behavior?

When using the Transition component with unmountOnExit, I expect the enter animation to work correctly.

Could you provide a CodeSandbox demo reproducing the bug?

https://codesandbox.io/s/admiring-fog-lhroc?file=/FadeScaleAlert.js

Most helpful comment

The enter animation should happen between the exited state and entering state, since the exited state is the first state that occurs when the component mounts.

Making the enter animation happen between the entering and entered state causes a delay of 300ms between when the user presses the button to open the alert and when the user actually starts seeing the alert fading, which is terrible user experience in my opinion.

All 11 comments

Is there anyone who knows why this bug is happening?

This is a very simple example of using the Transition component with unmountOnExit, but for some reason unmountOnExit caused the enter animation not to work.

Thanks for your time in advance!

Is there anyone who knows why this bug is happening?

This is a very simple example of using the Transition component with unmountOnExit, but for some reason unmountOnExit caused the enter animation not to work.

Thanks for your time in advance!

Is there anyone who knows why this bug is happening?

This is a very simple example of using the Transition component with unmountOnExit, but for some reason unmountOnExit caused the enter animation not to work.

Thanks for your time in advance!

May I ask if you have solved it

@parrot-designa no, I haven't solved it.

@parrot-designa no, I haven't solved it.

Do you have a social media account so we can talk to each other

@anatolzak Thank you for reporting this. I'll look into this.

@anatolzak Thank you for reporting this. I'll look into this.

Can it be fixed quickly? I don't remember this problem before

@anatolzak I've modified your CodeSandbox to work as expected. Is this your expected behavior?
https://codesandbox.io/s/still-thunder-82qud?file=/FadeScaleAlert.js

@koba04 that solution could work, but there is a 300ms delay between pressing the button and the scale animation starting visually.

The enter animation should happen between the exited state and entering state, since the exited state is the first state that occurs when the component mounts.

Making the enter animation happen between the entering and entered state causes a delay of 300ms between when the user presses the button to open the alert and when the user actually starts seeing the alert fading, which is terrible user experience in my opinion.

@koba04 thanks so much for working on this! I really appreciate your time and effort!

I had the same issue. I can suggest a workaround for this by dropping my code here. Pay attention to the setTimeout in the useEffect.

import {
  Transition as RTGTransition,
  TransitionGroup,
} from "react-transition-group";

const Animate = forwardRef(
  (
    {
      children,
      style,
      defaultStyle,
      transitionStyle,
      transitionStatus,
      className,
    }: any,
    outsideRef
  ) => {
    const s = useMemo(
      () => ({
        ...style,
        ...defaultStyle,
        //@ts-ignore
        ...transitionStyle[transitionStatus],
      }),
      [transitionStatus, defaultStyle, transitionStyle]
    );

    return (
      <div
        ref={(node) => {
          if (node) {
            if (outsideRef) {
              if (typeof outsideRef === "function") {
                outsideRef(node);
              } else {
                outsideRef.current = node;
              }
            }
          }
        }}
        className={className}
        style={s}
      >
        {transitionStatus}
        {children}
      </div>
    );
  }
);

const Transition = forwardRef(
  (
    {
      children,
      defaultStyle,
      transitionStyle,
      className,
      style,
      ...props
    }: any,
    outsideRef: any
  ) => {
    const [_in, _setIn] = useState(false);
    const timeoutRef = useRef<any>();

    useEffect(() => {
      // THIS IS THE IMPORTANT PART
      requestAnimationFrame(() => {
            timeoutRef.current = setTimeout(() => {
                _setIn(props.in);
            }, 5);
      })
      return () => {
        clearTimeout(timeoutRef.current);
      };
    }, [props.in, timeoutRef]);

    return (
      <RTGTransition {...props} in={_in}>
        {(transitionStatus) => {
          return (
            <Animate
              ref={(node) => {
                // ref.current = node;
                if (outsideRef) {
                  if (typeof outsideRef === "function") {
                    outsideRef(node);
                  } else {
                    outsideRef.current = node;
                  }
                }
              }}
              defaultStyle={defaultStyle}
              style={style}
              transitionStatus={transitionStatus}
              transitionStyle={transitionStyle}
            >
              {children}
            </Animate>
          );
        }}
      </RTGTransition>
    );
  }
);

const ScaleUpTransition = ({
  children,
  delay = 0,
  ease = "ease",
  from,
  to,
  style,
  timeout,
  ...props
}: any) => {
  return (
    <Transition
      {...props}
      timeout={timeout}
      style={style}
      defaultStyle={{
        transformOrigin: "center",
        willChange: "transform",
        transform: `scale(${from})`,
        transition: `transform ${timeout}ms ${ease} ${delay}ms`,
      }}
      transitionStyle={{
        entering: {
          transform: `scale(${to})`,
        },
        entered: {
          transform: `scale(${to})`,
        },
        exiting: {
          transform: `scale(${from})`,
        },
        exited: {
          transform: `scale(${from})`,
        },
      }}
      className={"ScaleUpTransition"}
    >
      {children}
    </Transition>
  );
};

const App = () => {
  const [animate, setAnimate] = useState(false);
  return (
    <div>
      <TransitionGroup>
        {animate ? (
          <ScaleUpTransition
            in
            timeout={2000}
            from={1}
            to={2}
            appear
            // mountOnEnter
            // unmountOnExit
          >
            some
          </ScaleUpTransition>
        ) : null}
      </TransitionGroup>
      <button
        onClick={() => {
          setAnimate(true);
        }}
      >
        click
      </button>
    </div>
  );
};

This is basically what #749 does.

Was this page helpful?
0 / 5 - 0 ratings