2

I've created a wrapper component to supply a list of class names via a string to support Tailwind utility transitions. This is intended to be nested in a TransitionGroup component to support animations of multiple items, like a list.

I'm able to animate out just fine, but entering is not working. Can you help me spot the bug in this code, please?

react 16.13.1 react-transition-group 4.4.1

Sandbox: https://codesandbox.io/s/delicate-feather-ymozq

(logging in there to watch the div reference in case the classes weren't being applied, but they seem to work fine)

import React, { ReactNode } from "react";
import { Transition as ReactTransition } from "react-transition-group";

interface TransitionProps {
  in?: boolean;
  timeout: number;
  enter?: string;
  enterFrom?: string;
  enterTo?: string;
  leave?: string;
  leaveFrom?: string;
  leaveTo?: string;
  children: ReactNode;
}

export function CSSTransition(props: TransitionProps) {
  const { enter, enterFrom, enterTo, leave, leaveFrom, leaveTo } = props;
  const nodeRef = React.useRef<HTMLDivElement>(null);

  const enterClasses = splitClasses(enter);
  const enterFromClasses = splitClasses(enterFrom);
  const enterToClasses = splitClasses(enterTo);
  const leaveClasses = splitClasses(leave);
  const leaveFromClasses = splitClasses(leaveFrom);
  const leaveToClasses = splitClasses(leaveTo);

  function splitClasses(string: string | undefined): string[] {
    if (string) return string.split(" ").filter((s) => s.length);
    return [];
  }

  function addClasses(classes: string[]) {
    nodeRef.current?.classList.add(...classes);
  }

  function removeClasses(classes: string[]) {
    nodeRef.current?.classList.remove(...classes);
  }

  return (
    <ReactTransition
      in={props.in}
      nodeRef={nodeRef}
      timeout={props.timeout}
      unmountOnExit
      onEnter={() => {
        console.log("onEnter", nodeRef);
        addClasses([...enterClasses, ...enterFromClasses]);
      }}
      onEntering={() => {
        console.log("onEntering", nodeRef);
        removeClasses(enterFromClasses);
        addClasses(enterToClasses);
      }}
      onEntered={() => {
        console.log("onEntered", nodeRef);
        removeClasses([...enterToClasses, ...enterClasses]);
      }}
      onExit={() => {
        console.log("onExit", nodeRef);
        addClasses([...leaveClasses, ...leaveFromClasses]);
      }}
      onExiting={() => {
        console.log("onExiting", nodeRef);
        removeClasses(leaveFromClasses);
        addClasses(leaveToClasses);
      }}
      onExited={() => {
        console.log("onExited", nodeRef);
        removeClasses([...leaveToClasses, ...leaveClasses]);
      }}
    >
      <div ref={nodeRef}>{props.children}</div>
    </ReactTransition>
  );
}
Steve
  • 592
  • 9
  • 24

1 Answers1

6

Solution:

In CSSTransition.tsx, change

import { Transition as ReactTransition } from "react-transition-group";

to

import { CSSTransition as ReactTransition } from "react-transition-group";

Explanation:

Tailwind class names rely on CSS animations.

Transition isn't designed to work with CSS animations as the onEnter and onEntering methods are called so quickly that a DOM repaint (a frame of the animation) doesn't occur between them. Thus, all the classes you set for enterTo are immediately applied without any chance to transition to them.

CSS Transition on the other hand, adds an -enter class as it creates the element, then adds an -enter-active on the next tick/frame. Finally, after the timeout, it removes all classes except for adding in a -done class.

The transition between the -enter and -enter-active class is the key to allowing CSS transitions to take place. One tick/frame is added between those classes which allows the library to force a repaint. This repaint forces the onEntering classes to transition as intended to the onEntered classes.

In other words, Transition switches to the enterTo state immediately whereas CSSTransition gives one frame between the class switch which is needed to allow the animation to take place.

The DOM usually animates/changes at a maximum of 60 frames per second (depending on the implementation/end-user screen refresh rate) leaving 16ms for each frame; it doesn't care about what happens between between those 16ms. If the final state after class changes is your enterTo state rather than your enterFrom state, it will ignore the enterFrom state and thus never show you the Tailwind animations.

Sources:

Zwei
  • 1,279
  • 8
  • 16
  • Of course, it's something this simple. Of course! Thank you so much. It's stumped me for days but in the process, I did learn a lot about how react-transition-group works. It works like a charm now. – Steve Jun 13 '20 at 16:20