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>
))
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.
For reference, here is a working example: https://github.com/pshrmn/react-router/commit/4d5f894a9343b9bbe6aca49b93dc996e8fdb702d
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:
The related section from the App render function:
But to balance that, here's how lazy-loaded, animated routes with authorizations look elsewhere in the project 馃槑 (Thank you, v4!)
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! 馃弮
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:
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)
HashHistory:
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).
Most helpful comment
You can do this now (or use
withRouter
but that ain't my style):or this: