React TransitionGroup - Detecting and cancelling a transition that is in progress

This is a crosspost of a question I posted on Stackoverflow. It hasn’t attracted any answers so I figured I would try posting here. I would appreciate the community’s input as I haven’t been able to find a solution anywhere thus far.

My app has multiple pages labelled ‘Red’, ‘Green’, ‘Blue’ and ‘Pink’. To allow the user to navigate between the pages, I use React Router (v5.2.0). The routes are wrapped in a TransitionGroup (v4.4.1) so that when the user navigates to a new page, a fade-in transition effect is applied to the page that makes it slowly fade into view. The duration of this transition is 5000ms.

**Problem**

The app initialises on page ‘Red’. Say the user clicks on page ‘Blue’ and then on page ‘Pink’ while Blue’s transition is still in progress. In this case, I would like to cancel Blue’s transition and just display the Pink page immediately, without applying any transition effect to it.

**Attempted solution**

My current solution makes use of a flag and conditional rendering:

class AnimationApp extends React.Component {
  constructor(props) {
    super(props);
    this.updateProgress = this.updateProgress.bind(this);
  }

  // Flag
  inProgress = false;
  updateProgress() {
    return (this.inProgress = !this.inProgress);
  };

  render() {
    const { location } = this.props;
    let currentRoute = (
      <Switch location={location}>
        <Route path="/hsl/:h/:s/:l" children={<HSL />} />
        <Route path="/rgb/:r/:g/:b" children={<RGB />} />
      </Switch>
    );
  
    // Conditional render
    if (this.inProgress) {
      updateProgress();
    } else {
      currentRoute = (
        <TransitionGroup>
          <CSSTransition
            key={location.key}
            classNames="fade"
            timeout={5000}
            onEnter={this.updateProgress}
            onEntered={this.updateProgress}
          >
            {currentRoute}
          </CSSTransition>
        </TransitionGroup>
      );
    }
  
    return (
      <div style={styles.fill}>
        <ul style={styles.nav}>
          <NavLink to="/hsl/10/90/50">Red</NavLink>
          <NavLink to="/hsl/120/100/40">Green</NavLink>
          <NavLink to="/rgb/33/150/243">Blue</NavLink>
          <NavLink to="/rgb/240/98/146">Pink</NavLink>
        </ul>
  
        <div style={styles.content}>{currentRoute}</div>
      </div>
    );
  }
  
}

AnimationApp = withRouter(AnimationApp)

Link to demo

The flag is a boolean class variable called inProgress. If an animation is in progress, its value is true, otherwise it is false. The value of this variable is flipped via a handler called updateProgress. This handler is called in three places - 1) onEnter (set to true), 2) onEntered (set to false) and 3) in the if-block of the conditional render statement below (set to false).

The conditional render statement checks the value of inProgress. If an animation is in progress (i.e. inProgress === true) at the time of re-render, then the route component is rendered by itself, without any transition effects, and inProgress is set to false. But if an animation is not in progress at re-render, then the route is wrapped in a TransitionGroup and a transition effect is applied to it on entry.

**Questions**

The above solution seems to work fine, but it is clumsy and I was wondering if there was a more concise way to achieve the desired result?

Also, the solution no longer works in the functional version of the component as shown below:

function AnimationApp() {
  let location = useLocation();

  // Flag
  let inProgress = false;
  const updateProgress = function () {
    return (inProgress = !inProgress);
  };

  let currentRoute = (
    <Switch location={location}>
      <Route path="/hsl/:h/:s/:l" children={<HSL />} />
      <Route path="/rgb/:r/:g/:b" children={<RGB />} />
    </Switch>
  );

  // Conditional render
  if (inProgress) {
    updateProgress();
  } else {
    currentRoute = (
      <TransitionGroup>
        <CSSTransition
          key={location.key}
          classNames="fade"
          timeout={5000}
          onEnter={updateProgress}
          onEntered={updateProgress}
        >
          {currentRoute}
        </CSSTransition>
      </TransitionGroup>
    );
  }

  return (
    <div style={styles.fill}>
      <ul style={styles.nav}>
        <NavLink to="/hsl/10/90/50">Red</NavLink>
        <NavLink to="/hsl/120/100/40">Green</NavLink>
        <NavLink to="/rgb/33/150/243">Blue</NavLink>
        <NavLink to="/rgb/240/98/146">Pink</NavLink>
      </ul>

      <div style={styles.content}>{currentRoute}</div>
    </div>
  );
}

Link to demo

If you click on Blue and then click on Pink before Blue’s transition has ended, then Pink is rendered twice. On the first render, it appears immediately without any transitions but on the second render, it appears with a transition. What’s causing the double render?