React-router: Animation Switch not compatible with react-transition-group v2

Created on 25 Jun 2017  路  27Comments  路  Source: ReactTraining/react-router

This is regarding - https://github.com/ReactTraining/react-router/issues/4351

So react-transition-group is superceeding react-addons-css-tranistion-group - https://github.com/reactjs/react-transition-group

Transition API v2 expects immediate children of <TransitionGroup> to be <CSSTransition>.

React Router <Switch> expects immediate children to be of type <Route>.

Does this mean it is not possible to do this with v2 transition api? I put some details/code here - https://github.com/reactjs/react-transition-group/issues/83

Most helpful comment

@DGizmo Seems like the problem is that ReactDOM.findDOMNode(this) returns null when your component switch to exit state.

As in facebook/react#4651, ReactDOM.findDOMNode will return null when the component is unmounted or the ref changes. Are you unmounting or re-referencing the DOM node inside your route?

I don't know enough about your setup to give a suggestion, but in my personal experience, I always found it very helpful to wrap everything inside CSSTransition with a div. This will not only ensure that CSSTransition always have a DOM node to work on (so findDOMNode won't return null), but also allow me to separate route transition and all the other animation within the route (otherwise, the animation will be applied to the outermost DOM node inside your route, which might not be what you want)

So here is how you can wrap your routes, notice the div with class WRAPPER:

 <TransitionGroup>
    <CSSTransition key={this.props.location.pathname.split('/')[1]} timeout={500} classNames="fadeTranslate" mountOnEnter={true} unmountOnExit={true}>
        <div className="WRAPPER">
            <Switch location={this.props.location}>
                <Route path="/" exact component={Home} />
                <Route path="/blog" component={Blog} />
                <Route path="/albumn" component={Albumn} />
            </Switch>
        </div>
    </CSSTransition>
</TransitionGroup>

And the classNames you give CSSTransition will be applied to this div:

image

Give it a try :). Let me know if the issue still persists.

All 27 comments

This is simple usage of v2 transition api:

import React, { Component } from 'react'
import { render } from 'react-dom'
import { TransitionGroup, CSSTransition } from 'react-transition-group'

import './index.css'

class App extends Component {
    state = {
        theme: 1
    }
    upTheme = () => this.setState(({ theme }) => ({ theme:theme+1 }));
    render() {
        const { theme } = this.state;
        return (
            <TransitionGroup className="rawr">
                <CSSTransition appear timeout={{appear:1000,enter:2000,exit:1000}} classNames="pagefade" key={theme.toString()}>
                    <span className="mydiv" onClick={this.upTheme}>{theme}</span>
                </CSSTransition>
            </TransitionGroup>
        );
    }
}

CSS:

.pagefade-enter, .pagefade-appear { opacity:0; }
.pagefade-enter.pagefade-enter-active,
.pagefade-appear.pagefade-appear-active { opacity:1; transition:opacity 1000ms; }
.pagefade-enter.pagefade-enter-active { transition-delay:1000ms; }
.pagefade-exit { opacity:1; }
.pagefade-exit.pagefade-exit-active { opacity:0; transition:opacity 1000ms; }

Thanks @timdorr for the fast reply when you are so busy. However this article is not related to react-transition-group v2 - it is very different. Please don't mind the thumbs down, I just don't want others going there and wasting their time.

You need to make sure you're assigning key props to everything. That's the trick.

Not sure whether this is what you want, but here is how I use react-transition-group v2 with react-router v4:

<TransitionGroup>
    <CSSTransition key={this.props.location.key} timeout={500} classNames="fading-animation-transition" mountOnEnter={true} unmountOnExit={true}>
        <Switch location={this.props.location}>
            <Route path="/" exact component={Home} />
            <Route path="/blog" component={Blog} />
            <Route path="/albumn" component={Albumn} />
        </Switch>
    </CSSTransition>
</TransitionGroup>

Thank you so so much @Horizon-Blue !! That is exactly what I was trying to do. Thanks so much!

Thank you it works well !

There is still the case when you have sub routes, the transition will be triggered on every sub routes changes.

@hlehmann Yeah, that's because location.key keeps changing every time the route gets an update. To solve this problem, you can try using this.props.location.pathname instead of this.props.location.key, and using split() to get the prefix. e.g.

<TransitionGroup>
    <CSSTransition key={this.props.location.pathname.split('/')[1]} timeout={500} classNames="fading-animation-transition" mountOnEnter={true} unmountOnExit={true}>
        <Switch location={this.props.location}>
            <Route path="/" exact component={Home} />
            <Route path="/blog" component={Blog} />
            <Route path="/albumn" component={Albumn} />
        </Switch>
    </CSSTransition>
</TransitionGroup>

This will trigger the animation if I switch from /blog to /albumn, but will not trigger if I switch from /blog to /blog/post

Yes that's the solution I came out. But maybe a more integrated solution like a could avoid this trick.

@Horizon-Blue It works, but trow this error. Don't know what's wrong.

removeClass.js:4 Uncaught TypeError: Cannot read property 'classList' of null
    at removeClass (removeClass.js:4)
    at CSSTransition.removeClasses (CSSTransition.js:267)
    at Object.CSSTransition._this.onExit (CSSTransition.js:228)
    at Transition.performExit (Transition.js:250)
    at Transition.updateStatus (Transition.js:195)
    at Transition.componentDidUpdate (Transition.js:159)
    at measureLifeCyclePerf (ReactCompositeComponent.js:75)
    at eval (ReactCompositeComponent.js:729)
    at CallbackQueue.notifyAll (CallbackQueue.js:76)
    at ReactReconcileTransaction.close (ReactReconcileTransaction.js:80)
    at ReactReconcileTransaction.closeAll (Transaction.js:206)
    at ReactReconcileTransaction.perform (Transaction.js:153)
    at ReactUpdatesFlushTransaction.perform (Transaction.js:140)
    at ReactUpdatesFlushTransaction.perform (ReactUpdates.js:89)
    at Object.flushBatchedUpdates (ReactUpdates.js:172)
    at ReactDefaultBatchingStrategyTransaction.closeAll (Transaction.js:206)
    at ReactDefaultBatchingStrategyTransaction.perform (Transaction.js:153)
    at Object.batchedUpdates (ReactDefaultBatchingStrategy.js:62)
    at Object.batchedUpdates (ReactUpdates.js:97)
    at dispatchEvent (ReactEventListener.js:147)

@DGizmo Seems like the problem is that ReactDOM.findDOMNode(this) returns null when your component switch to exit state.

As in facebook/react#4651, ReactDOM.findDOMNode will return null when the component is unmounted or the ref changes. Are you unmounting or re-referencing the DOM node inside your route?

I don't know enough about your setup to give a suggestion, but in my personal experience, I always found it very helpful to wrap everything inside CSSTransition with a div. This will not only ensure that CSSTransition always have a DOM node to work on (so findDOMNode won't return null), but also allow me to separate route transition and all the other animation within the route (otherwise, the animation will be applied to the outermost DOM node inside your route, which might not be what you want)

So here is how you can wrap your routes, notice the div with class WRAPPER:

 <TransitionGroup>
    <CSSTransition key={this.props.location.pathname.split('/')[1]} timeout={500} classNames="fadeTranslate" mountOnEnter={true} unmountOnExit={true}>
        <div className="WRAPPER">
            <Switch location={this.props.location}>
                <Route path="/" exact component={Home} />
                <Route path="/blog" component={Blog} />
                <Route path="/albumn" component={Albumn} />
            </Switch>
        </div>
    </CSSTransition>
</TransitionGroup>

And the classNames you give CSSTransition will be applied to this div:

image

Give it a try :). Let me know if the issue still persists.

Now it works without errors, thank you @Horizon-Blue! But because of the additional wrapper layer I had to rework my animations. Well, now it looks complex :)

Thank you very much @Horizon-Blue for your help on this. The article linked on hackernoon in the comment above - https://github.com/ReactTraining/react-router/issues/5279#issuecomment-310946205 - is not useful for react-transition-group v2 - so I really appreciate you giving this help.

This was very helpful @Horizon-Blue. Any thoughts on how one might achieve different transitions for each route?

@andrewcroce I can't think of any elegant way to approach this... though one thing you can try is to define a custom map from pathname to corresponding css transition effect, e.g.:

const transitionMap = {
    blog: { classNames: 'fade-translate', timeout: 500 },
    albumn: { classNames: 'fade-in-out', timeout: 300 },
    account: { classNames: 'fade-translate', timeout: 550 },
};

and in your route definition:

render = () => {
    const current = this.props.location.pathname.split('/')[1];
    return (
        <TransitionGroup>
            <CSSTransition
                key={current}
                classNames="default-transition"
                timeout={300}
                {...transitionMap[current]}
                mountOnEnter={true}
                unmountOnExit={true}
            >
                <div>
                    <Switch location={this.props.location}>
                        <Route path="/blog" component={Blog} />
                        <Route path="/albumn" component={Albumn} />
                        <Route path="/about" component={About} />
                        <Route path="/account" component={Account} />
                    </Switch>
                </div>
            </CSSTransition>
        </TransitionGroup>
    );
};

If there is no special transition defined for a route, then default-transition will be used. Otherwiese, {...transitionMap[current]} will override previously defined transitions.

This might not be the best way to solve the problem, but it should work... :P

Thanks so much @Horizon-Blue to help us brainstorm these ideas. I also have a need in the future for varied transitions per route, I will share my brainstorm here when I get to work on that. :)

Thanks @Horizon-Blue. This is a helpful idea.

Here is a working fade page transition example with React Router v4 and React Transition Group v2: https://codesandbox.io/s/4RAqrkRkn?view=preview

Related issue on how it was created.

Thanks for your help @Horizon-Blue .Now the page sliding animation is OK.

const RouterMap = () => (
  <Router>
    <Route render={({ location }) =>
      <TransitionGroup>
        <CSSTransition key={location.pathname.split('/')[1]} timeout={500} classNames="pageSlider" mountOnEnter={true} unmountOnExit={true}>
          <Switch location={location}>
            <Route path="/" exact component={ Index } />
            <Route path="/comments" component={ Comments } />
            <Route path="/opinions" component={ Opinions } />
            <Route path="/games" component={ Games } />
          </Switch>
        </CSSTransition>
      </TransitionGroup>
    } />
  </Router>
)

And the css:

.pageSlider-enter {
  transform: translate3d(100%, 0, 0);
}

.pageSlider-enter.pageSlider-enter-active {
  transform: translate3d(0, 0, 0);
  transition: all 600ms;
}
.pageSlider-exit {
  transform: translate3d(0, 0, 0);
}

.pageSlider-exit.pageSlider-exit-active {
  transform: translate3d(-100%, 0, 0);
  transition: all 600ms;
}

The animation is as bellow:

pageslidinganimation

As you see, the animation that index page slide to the detail page is all right. But when I click the Back icon, I hope 'index page' comes out from left to right.

I know if I change the CSS as bellow, the page will come out from left to right:

.pageSlider-enter {
  transform: translate3d(-100%, 0, 0);
}
.pageSlider-exit.pageSlider-exit-active {
  transform: translate3d(100%, 0, 0);
  transition: all 600ms;
}

But how combine the two animations together? Generally speaking, whenever user clicks the back icon, the animation should be from left to right. Do you have any idea? Many thanks.

Hi @jasonintju , you could try something similar to the method discussed in https://github.com/ReactTraining/react-router/issues/5279#issuecomment-316877263 to specify a different class for the root page.

e.g.

const isRoot = location.pathname === '/';

and then

<CSSTransition key={location.pathname.split('/')[1]} timeout={500} classNames={isRoot ? 'left-to-right' : 'right-to-left'} mountOnEnter={true} unmountOnExit={true}>

For my purpose I recently created a module based on react-css-transtion-replace here: https://github.com/LKay/react-transition-replace to animate between routes.

Then I just created a SwitchTransition component which handles route change just like regular Switch:

const SwitchTransition = withRouter(
    ({
        children,
        classNames,
        location,
        match : matchProps,
        transition : Transition,
        ...props
    }) => {
        let match, child;

        React.Children.forEach(children, (element) => {
            if (!React.isValidElement(element)) return;

            const { path: pathProp, exact, strict, from } = element.props;
            const path = pathProp || from;

            if (!match) {
                child = element;
                match = path ? matchPath(location.pathname, { path, exact, strict }) : matchProps;
            }
        });

        return (
            <TransitionReplace
                classNames={ classNames }
                timeout={ 500 }
                overflowHidden
            >
                { match ? (
                    <Transition
                        key={ match.path }
                        mountOnEnter
                        unmountOnExit
                        { ...props }
                    >
                        { cloneElement(child, { location, computedMatch: match }) }
                    </Transition>
                ) : null }
            </TransitionReplace>
        )
    }
);

And the example use:

<SwitchTransition
    transition={ SlideTransition }
    classNames="slide"
>
    <Route path="/step1" component={ Step1 }  />
    <Route path="/step2" component={ Step2 }  />
</SwitchTransition>

Rebound off of @LKay's code for ReactCSSTransitionGroup:

import React, { cloneElement } from 'react';
import { withRouter, matchPath, Route } from 'react-router-dom';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';

class _TransitionSwitch extends React.Component {
    render() {
        const {
            children,
            classNames,
            location,
            match: matchProps,
            transitionProps,
            ...props
        } = this.props;

        let match, child;

        React.Children.forEach(children, element => {
            if (!React.isValidElement(element)) return;

            const { path: pathProp, exact, strict, from } = element.props;
            const path = pathProp || from;

            if (!match) {
                child = element;
                match = path
                    ? matchPath(location.pathname, { path, exact, strict })
                    : matchProps;
            }
        });

        return (
            <Route
                render={({ location }) =>
                    <ReactCSSTransitionGroup {...transitionProps}>
                        {cloneElement(child, {
                            key: match.path || 'default',
                            location,
                            computedMatch: match
                        })}
                    </ReactCSSTransitionGroup>}
            />
        );
    }
}

export const TransitionSwitch = withRouter(_TransitionSwitch);

I thought those that took a gander at the hackernoon article and are looking to animate transitions with React Router would, like me benefit from a revisit with React Transition Group v2:

https://github.com/gianlucacandiotti/react-router-transitions

This repo reconciles the diff and uses animate.js instead of a CSS child (which has greater React Native parity)

A good explanation can be found in this article:

Children Component Types

In the previous version of react-transition-group, the children of a could be any component type. However, in v2, these should be either <Transition>s or <CSSTransition>s. Also, transition props (transitionName, transitionEnterTimeout, etc.) were placed on the <TransitionGroup> in previous versions. In v2, these props are placed on the <Transition>/<CSSTransition> components.

If you have some kind of authorization to protect a route and try to access it, a redirection warning is given. It works, but it's always giving that warning, am I doing something wrong?

<TransitionGroup>
  <CSSTransition key={location.key} classNames="fade" timeout={2000}>
    <Switch location={location}>
      <Route
        exact
        path="/"
        render={props =>
          !this.state.loggedIn ? (
            <PageOne />
          ) : (
            <div>
              <Redirect to="/page2" />
            </div>
          )
        }
      />
      <Route
        path="/page2"
        render={props =>
          this.state.loggedIn ? (
            <PageTwo />
          ) : (
            <div>
              <Redirect to="/" />
            </div>
          )
        }
      />
      <Route path="/page3" component={PageThree} />
    </Switch>
  </CSSTransition>
</TransitionGroup>

Thanks for this thread. It helped me a lot to understand my problem.

@andrewcroce here is the way I do to handle multiple transitions (in my case there can be multiple leaving transitions on the same page depending on the next state):

On the changing page trigger I push a new location to the history with a state:

<Button
    onClick={() => {
        history.push({
            pathname: '/next',
            state: {transition: 'fade', duration: 1000}
        })
    }
/>
    Go to next page
</Button>

Then I declared a Transition component (to understand this code you need to read this issue that was the end of my search for a solution):

import React from 'react'
import { TransitionGroup, CSSTransition } from 'react-transition-group'

const childFactoryCreator = (props) => child => React.cloneElement(child, props)

export const Transitions = ({ transition, duration, pageKey, children, ...props }) => (
  <TransitionGroup
    childFactory={childFactoryCreator({ classNames: transition, timeout: duration })}
    {...props}
  >
    <CSSTransition
      key={pageKey}
      classNames={transition}
      timeout={duration}
    >
      { children }
    </CSSTransition>
  </TransitionGroup>
)

Then on the router component:

withRouter(({ location }) => (
    // default is no transition. override if location has a state
    <Transitions pageKey={location.pathname} transition='' duration={0} {...location.state}>
        <Switch location={location}>
            <Route path='/home' component={Home} />
            <Route path='/next' component={Next} />
        </Switch>
    </Transitions>
))

Hi. I followed the advice in this thread to get my Routes animating. I am having an issue with the <Redirect> component. Redirecting works, but I still get a warning in the console:

Warning: You tried to redirect to the same route you're currently on: "/"

The warning is visible in this code sandbox when you enter a bad route. Is there another way I should use the Redirect?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tomatau picture tomatau  路  3Comments

jzimmek picture jzimmek  路  3Comments

andrewpillar picture andrewpillar  路  3Comments

misterwilliam picture misterwilliam  路  3Comments

ArthurRougier picture ArthurRougier  路  3Comments