React-router: Animating with <Switch> using previous pathname

Created on 18 Jan 2017  路  44Comments  路  Source: ReactTraining/react-router

Animation with <Switch> is a little bit confusing. The goal is to animate the entering and leaving <Route>s, but what you have to do is animate the <Switch>. Unfortunately, when a <Switch> is rendered, it grabs the most current history.location.pathname. This means that both entering and exiting <Switch>es are choosing which <Route> to render based on the current location. I was able to get around that by adding a pathname prop to <Switch>, which will prefer that over grabbing the current history.location.pathname.

const Routes = withRouter(({ history }) => (
  <CSSTransitionGroup
    transitionName='fade'
    transitionEnterTimeout={500}
    transitionLeaveTimeout={500}
    >
    <Switch key={history.location.pathname} pathname={history.location.pathname}>
      <Route path="/red" render={Red} />
      <Route path="/green" render={Green} />
      <Route path="/blue" render={Blue} />
    </Switch>
  </CSSTransitionGroup>
))
bug feature

Most helpful comment

You can do this now (or use withRouter but that ain't my style):

<Route render={({ location }) => (
  <CSSTransitionGroup
    transitionName="example"
    transitionEnterTimeout={500}
    transitionLeaveTimeout={500}
  >
    <Switch key={location.key} location={location}>
      <Route path="/red" component={Red}/>
      <Route path="/green" component={Green} />
      <Route path="/blue" component={Blue} />
    </Switch>
  </CSSTransitionGroup>
)}/>

or this:

<Route render={({ location }) => (
  <CSSTransitionGroup
    transitionName="example"
    transitionEnterTimeout={500}
    transitionLeaveTimeout={500}
  >
    <Route
      path="/:color"
      component={Color}
      location={location}
      key={location.key}
    />
  </CSSTransitionGroup>
)}/>

All 44 comments

What if this was moved up to withRouter? It would automatically enable this for normal Routes too. Just thinking out loud, so it may not be practical

@mjackson mentioned destructuring the history object in context.router https://github.com/ReactTraining/react-router/issues/4134#issuecomment-274286037. With that, a component could implement getChildContext and insert a different location in context.router, and that would be the "current" location to any children that access it.

I believe that <Switch> and <Route> would still need to allow a location prop (I used pathname in my example, but location is more practical) because reliance on context is what is causing this issue.

Anyways, animating leaving elements is weird. I understand how it works with react-transition-group, but then I look at examples of react-motion's <TransitionMotion> and feel like I'm trying to read using a mirror. I should probably give in and actually read the source.

How does <Switch pathname> fix this?

Both your <Routes> component and <Switch> use withRouter, which means they should both get the new location at (approximately) the same time. I guess I'm just trying to make sure that this fix isn't based on an implementation detail, like the order in which withRouter registers listeners.

So given the above route structure, if we are at the location /red, we are effectively rendering:

<CSSTransitionGroup>
  <Switch><Red /></Switch>
</CSSTransitionGroup>

and if the user clicks a link to /blue, then we _should_ render:

<CSSTransitionGroup>
  <Switch><Red className='fade-leave'/></Switch>
  <Switch><Blue className='fade-enter'/></Switch>
</CSSTransitionGroup>

but both <Switch> components are using context.history.location.pathname to determine which <Route> to render, so instead we end up with:

<CSSTransitionGroup>
  <Switch><Blue className='fade-leave'/></Switch>
  <Switch><Blue className='fade-enter'/></Switch>
</CSSTransitionGroup>

Now, if <Switch> can take a location prop, then we can prefer to use that over the one available through the context.

pathname = location ? location.pathname : history.location.pathname
for (...) {
  match = matchPath(pathname, ...)
}
<CSSTransitionGroup>
  <Switch pathname='/red'><Red className='fade-leave'/></Switch>
  <Switch pathname='/blue'><Blue className='fade-enter'/></Switch>
</CSSTransitionGroup>

Yep, that makes sense. But it seems like in your initial example you're getting the value of <Switch pathname> directly from history.location.pathname, which should already contain the new location because the history object updates in place...

The history object does, but not the location object.

Thanks for the example code. I'm about to push some code that consolidates context.history and context.match into a single object on context.router as I hinted at in https://github.com/ReactTraining/react-router/issues/4134#issuecomment-274286037. I'm keen to dig further into this issue after that code lands.

Updated the example to work with the destructured context.router props. https://github.com/pshrmn/react-router/commit/6d78791080da9e19aa53306f1f8db7fc980d6b5c

With the changes we made in 914956ee82574862c0f52e59b1409b8c839fa86f this should work fine:

<Route render={({ location }) => (
  <TransitionGroup>
    <Switch key={location.key}>
      {/* routes */}
    </Switch>
  </TransitionGroup>
)}/>

The render callback gives you the same thing as withHistory so you rarely need withHistory. I've still not needed withHistory anywhere.

I'd like to change the animation example to use TransitionGroup, TransitionMotion is about the hairiest component most people ever have to deal with! (though it is incredibly powerful)

@ryanflorence When a <Switch> is leaving, it is still listening for location changes, so state.location ends up being set as the new location and the leaving <Switch> renders the new <Route>. Providing a location prop that is preferred over state.location gets around this.

Perhaps there is another way to get around this without using a prop, but I haven't come up with a good one.

react-transition-group is definitely easier to understand than react-motion, so I think that it makes sense to switch to that in the animation example.

Another semi-related question: the suggested "fix" works for the example provided, but how would one treat a case where each route in the <Switch> needs to be animated differently? Is that even possible?

Not with this approach, no. That would be a transition child element having responsibility over the transition group, the parent. You'd probably want to have your animation inside of your Route then, similar to the animated transition in the docs. In my projects I've got a custom Route component that I use overtop of the one from the Router that has the ability to take its transition (and permission restriction) as props. It works well, but I make the tradeoff of not being able to use Switch

@lourd Thats a bit unfortunate. Leaves me in a tricky position, since I quite heavily rely on the <Switch> to handle when to redirect a user after typing an invalid URL and similar. Without the <Switch> I'd have to implement this logic manually, which feels a bit ugly (like checking that any of the other routes did not match, etc). I'll have to look into it further and see what I can come up with.

Yup, you're right. Here's a couple screenshots from a current project showing what it looks like:

The functions for manually checking:
screen shot 2017-02-19 at 5 22 56 pm

The related section from the App render function:
screen shot 2017-02-19 at 5 23 43 pm

But to balance that, here's how lazy-loaded, animated routes with authorizations look elsewhere in the project 馃槑 (Thank you, v4!)
screen shot 2017-02-19 at 5 24 55 pm

I was just goofing around with this. I'd like to be able to provide a location prop to Switch, Route and anything else that listens to history.

You can think of it like a controlled v. uncontrolled input. A controlled input get's it's value from the app. A "controlled" Route or Switch gets its location from the App, and uncontrolled one gets it from the router.

This will allow animations and anything else that assumes React's normal flow of data (props) to work, letting us opt-out of the subscriptions at every level.

closed by 143db65b04c34a2b8b13d263d304f350977dcedb

You can do this now (or use withRouter but that ain't my style):

<Route render={({ location }) => (
  <CSSTransitionGroup
    transitionName="example"
    transitionEnterTimeout={500}
    transitionLeaveTimeout={500}
  >
    <Switch key={location.key} location={location}>
      <Route path="/red" component={Red}/>
      <Route path="/green" component={Green} />
      <Route path="/blue" component={Blue} />
    </Switch>
  </CSSTransitionGroup>
)}/>

or this:

<Route render={({ location }) => (
  <CSSTransitionGroup
    transitionName="example"
    transitionEnterTimeout={500}
    transitionLeaveTimeout={500}
  >
    <Route
      path="/:color"
      component={Color}
      location={location}
      key={location.key}
    />
  </CSSTransitionGroup>
)}/>

Actually check this out:

const CSSTransitionRoute = ({ transitionName, ...rest }) => (
  <Route render={({ location }) => (
    <CSSTransitionGroup
      transitionName={transitionName}
      transitionEnterTimeout={500}
      transitionLeaveTimeout={500}
    >
      <Route
        location={location}
        key={location.key}
        {...rest}
      />
    </CSSTransitionGroup>
  )}/>
)

Transitions usually need some sort of relationship between screens to know what to do. In this case, we're only creating a transition for "siblings" which is quite common! (same path, different params, like the detail page of a master/detail UI.)

<div>
  <Master>
    <UsersList/>
  </Master>
  <Detail>
    <CSSTransitionRoute
      transitionName="fade"
      path="/users/:id"
      component={User}
    />
  </Detail>
</div>

Copied from basic example from docs into a create-react-app and added the animation suggestion above:

https://gist.github.com/joefraley/00a1bc702ecc60cbf0844db8e670ad6c

//......
      <Route render={({location}) => (
        <ReactCSSTransitionGroup 
          transitionName="example"
          transitionEnterTimeout={300}
          transitionLeaveTimeout={300}
        >
          <Switch key={location.key} location={location}>
            <Route exact path="/" component={Home}/>
            <Route exact path="/about" component={About}/>
          </Switch>
        </ReactCSSTransitionGroup>

      )} />
//........

I feel like...I'm not understanding this discussion.

I feel like...I'm not understanding this discussion.

@joefraley - I was in the same boat as you, it was confusing. What really clicked for me is that there are two location's, (1) history.location and now (2) location.

More detailed answer: The react-element rendered by <Switch> or <Route> gets three props. location, history, and match. The history prop has a location property but this is a live object, connected to context. The non history.location, thus location prop is immutable, therefore it can be used with Redux and ReactCSSTransition.

I don't understand why we supply a location attribute to a <Switch> though.

@Noitidart If you don't give the <Switch> a location, then it will use the one from the context. That is fine for the entering <Switch>, but the leaving <Switch> should be rendering components based on the previous location.

@joefraley I just did a fresh CRA, yarn install react-router-dom@next react-addons-css-transition-group and then copy/pasted your gist. It's working as designed. I think you're just on an older beta. GOTTA KEEP UP! 馃弮

zwf5rtymjl

Tried something similar with react-motion, because we have to use different transitions for specific routes. I just ported our react-router v2 version and applied all things I learned from this issue, but I cant get it working. Do you have an advice for me?

https://www.webpackbin.com/bins/-Kf7Mlio2VC2NogKwM8A

Expected behavior would be:

  • on load you see the green home container
  • after click on "Route 1" the red box slides in from right, the green box stays and gets overlapped
  • after click on "Home" the red box slides out to the right and the green box appears

Is there an example of page transitions on react-native. It seems the CSSTransitionGroup doesn't work on native.

it seems that this does not work with HashRouter. Any ideas on how to make it work?

Has anyone tried this with the ReactTransitionGroup hooks?

I have the ReactCSSTransitionGroup example above working but when switching to the low-level API none of the hooks are firing.

Here is an example of how I got this working using ReactTransitionGroup.

import React, { Component, PropTypes } from 'react';
import { matchPath, withRouter } from 'react-router';
import ReactTransitionGroup from 'react-addons-transition-group';
import Home from './Home';
import Products from './Products';
import NotFound from './NotFound';

const routes = [
  {
    path: '/',
    component: Home,
    exact: true,
  },
  {
    path: '/products',
    component: Products,
    exact: true,
  },
  {
    path: '/products/:productId',
    component: Products,
    exact: true,
  },
  {
    component: NotFound,
  },
];

@withRouter
export default class AppRoutes extends Component {
  static propTypes = {
    location: PropTypes.shape({ pathname: PropTypes.string }).isRequired,
  };

  state = {
    matchedRoutes: [],
  };

  componentWillMount() {
    this.matchRoutes(this.props.location);
  }

  componentWillReceiveProps(nextProps) {
    this.matchRoutes(nextProps.location);
  }

  matchRoutes = ({ pathname }) => {
    const matchedRoutes = [];

    for (let i = 0; i < routes.length; i += 1) {
      const { component: RouteComponent, ...rest } = routes[i];

      const match = matchPath(pathname, { ...rest });

      if (match) {
        matchedRoutes.push(
          <RouteComponent key={RouteComponent} {...match} />,
        );

        if (match.isExact || match.isStrict) {
          break;
        }
      }
    }

    this.setState({ matchedRoutes });
  };

  render() {
    return (
      <ReactTransitionGroup>
        {this.state.matchedRoutes}
      </ReactTransitionGroup>
    );
  }
}

Basically I wrote my own Switch function (matchRoutes) which will iterate through a route config and use matchPath to validate the routes. If we find a match, we forward the match as props to the component being used in the transition group and push that component into an array in local state which will get rendered as a child of the transition group. This allows for our transition components to receive all of the low-level transition hooks (componentWillAppear, componentWillLeave etc.).

Hey @ryanflorence in reference to your comment (where you say withRouter ain't your style :p) I got your Switch approach working with a CSSTransition which I am thrilled about! I'm so sorry for asking such a dumb question but do you mind telling me where I can read about how CSSTransitionGroup is interacting with Switch and Route here? Particularly I cant wrap my head around why providing key and location solve the problem, and why the outer <Route render={({ location }) => ... /> is necessary either. Thanks!

hmm, actually I'm getting Uncaught TypeError: Cannot read property 'componentWillEnter' of undefined with this when I go back and then forward again:

    <Router>
      <Route render={({ location }) => (
        <ReactCSSTransitionGroup
          transitionName="route"
          transitionAppear={true}
          transitionAppearTimeout={300}
          transitionEnterTimeout={300}
          transitionLeaveTimeout={300}
          transitionEnter={true}
          transitionLeave={true}>
          <Switch key={location.key} location={location}>
            <Route exact path="/" component={Home} />
            <Route path="/test" component={Test} />
          </Switch>
        </ReactCSSTransitionGroup>
      )}/>
    </Router>

@pshrmn @ryanflorence

I've tried all the suggestions here (using react-router 4.1.1), while I get in/out transitions, it allows the components to be rendered multiple times (opened #5111 about this, since the animation demo in the docs also exhibits this behavior)

I've created a codepen which illustrates this: https://codepen.io/asherwin/pen/eWKYgr

In this case, I'm just trying to animate in/out a sub-menu (like an accordion, essentially), but if you click the route links multiple times (rapidly click on "menu one") you'll see that it renders the sub menu multiple times

I've tried implementing a custom function based on the <Route children={...}> property, however match is always set so I can't figure out a way to differentiate

I feel like I'm doing something fundamentally wrong here.. if someone could point it out, that would be great

The expected behavior would be once "menu one" was open (sub menu displayed), if you clicked on "menu one" again, nothing at all would happen

As @ulich said before, react-transition-group is not working with HashHistory.

Here two example.

BrowserHistory (as expected)
browser

HashHistory:
hash

Here the code: https://github.com/Takeno/react-router-transition-poe

Any ideas? Is it a bug?

@Takeno, the behavior is because hash history locations don't have a key. From the HashRouter docs

IMPORTANT NOTE: Hash history does not support location.key or location.state. In previous versions we attempted to shim the behavior but there were edge-cases we couldn鈥檛 solve. Any code or plugin that needs this behavior won鈥檛 work. As this technique is only intended to support legacy browsers, we encourage you to configure your server to work with instead.

The key is needed to differentiate between the TransitionGroup component's child Switch components. If you can't switch to using the browser history API, you'll need to figure out a different key value. Possibly the location's pathname or hash?

@alex-sherwin, your example is kind of related to this. You're seeing the multiple renders in your codepen because every Link click generates a new location, with a new key. If you swap out location.key for location.pathname as the prop to both Switch components in your Codepen example, it works as expected.

@ryanflorence Are there any plans in the future to have an example of Animated route transitions on the react router website that would be most helpful to newbies like me and others in general :)

In the interim, I went ahead and made a thing demonstrating what we've all been discussing and learning. It started as me trying to debug missing transitions in my app. Once I nailed the bug I figured I'd go ahead and publish it since it helped me sort out my own misunderstandings. Hope y'all find it helpful 猸愶笍

@ryanflorence, let me know how I can help incorporate any part of the explanation or example code into the docs. Explaining that Switch and Route take a location prop for certain scenarios is especially confusing, and missing from the docs currently. The behavior with Switch passing down its location to its matched child component is what bit me in the ass with the bug I was investigating.

@ryanflorence @lourd I checked your thingy for the transitions and they do work. I have an almost "identical" setup and I can't get it to work.

Like this:

<BrowserRouter>
    <div className="whatever">
        <Navbar />
        <div className="container-fluid">
        <CSSTransitionGroup transitionName="fade"
        transitionEnterTimeout={2000} transitionLeaveTimeout={2000}>
        <Switch key={location.pathname} location={location}>
            <Route exact path="/" component={Home} />
            <Route path="/about" component={About} />
        </Switch>
        </CSSTransitionGroup>
    </div>
    </div>
</BrowserRouter>

I have the css setup but the animations don't play out. The Links are in the Navbar component and they have basically:

<Link to="/" />

// or

<Link to="/about" />

What am I missing? I'm going crazy over this

edit - I realised the location.pathname does not update after a Link.to navigation. I must be doing something wrong

@JoaoCnh, where is location coming from in your example? If any component is using the connect decorator from react-redux or something else possibly using shouldComponentUpdate then you'll need to use withRouter decorator in the right place to "pierce through" that check.

@lourd First of all thanks for helping out, I managed to get it working BUT don't know if it's a good thing or a bad thing whenever I go from Home to About it renders like this:

render about
render about
render about

the animation presents no flickering and things look smooth but why does it render about 3 times? o.O

my code is looking like this:

<BrowserRouter>
    <div className="tracker">
        <Navbar />
        <div className="container-fluid">
        <Page />
        </div>
    </div>
</BrowserRouter>

and then Page:

@withRouter
export default class Page extends Component {
  render() {
    return (
      <CSSTransitionGroup transitionName="fade"
        transitionEnterTimeout={500} transitionLeaveTimeout={300}>
        <Switch key={location.pathname} location={location}>
          {routes.map(route => <Route key={route.path} {...route} />)}
        </Switch>
      </CSSTransitionGroup>
    );
  }
};

In the Navbar component I'm unsing NavLink by the way

@JoaoCnh You are using window.location not this.props.location. I'm not sure if that is causing your problems, but it _is_ a problem.

Also, generally speaking usage questions should be posted to StackOverflow or Reactiflux. Otherwise the maintainers and everyone who has posted in a thread gets a bunch of notifications (mostly) unrelated to the original issue.

@pshrmn @lourd I'm sorry but this issue is just driving me crazy. I was indeed using the wrong thing but with this.props.location when I change from Home to About the render pattern changes to:

render About
render Home
render About

Any more problemas and I'll make a SO question for this and link it here

@lourd many thanks for your example, really helpful and useful to study.

im having this issue... tried using this.props.location also windows.location but the same result...

This issue has been solved and closed for a while, but it has since turned into a clearinghouse for questions related to animation, so I am going to lock this.

For anyone coming here to see _how_ to do animation, please see Ryan's comment here: https://github.com/ReactTraining/react-router/issues/4351#issuecomment-281196606.

For anyone coming here with usage questions, please ask them on StackOverflow or in the Reactiflux discord chat (there is a #react-router room there).

Was this page helpful?
0 / 5 - 0 ratings