Closed vladshcherbin closed 7 years ago
So i'm not really familiar with how RR4 works, but to animate the things you can do something like this: https://codesandbox.io/s/wmEJoD5oM I don't know why the "old" route updates showing the same new page twice, but that is down to how RR4 works. I tried to prevent updates on the exiting component but the router is bypassing shouldComponentUpdate higher in the component tree so there isn't much i can do. Maybe others know more
I was facing a similar issue while developing my blog. I ended up using something like this: https://codesandbox.io/s/YE6l8EmR9
A few highlights:
location
prop is pass to Switch
. I haven't check the implementation of RR4 yet, but setting location={this.props.location}
simply works.section
whose position is fixed
at all time.The link above includes more details on how the CSS is implemented. I used a fixed timeout for all transitions and didn't include any delay, but it should be fairly easy to extend on this setup.
Edit: example for custom delay: https://codesandbox.io/s/xvjlnGMnr
Yes, Switch
is rendering the component according to the current
location, so when the component is exiting the location is already changed and another element is rendered according to it. So, we can get current location and pass it to Switch
so it won't read new location and stay the same for the whole component lifecycle. [Solved]
I'm also using fixed
section wrapper.
So, the only remaining thing is transition animation. I'd love to explore two ways:
1) native (with CSSTransition and classes) like in examples above 2) external animation library (I tried Anime, has great docs and is easy to use) and Transition.
The 1 one seems easy, the 2 one can probably be done with onEnter
, onExit
functions as they return the rendered element, which is what anime wants.
I'll post the result here when I finish.
@Horizon-Blue thanks for the sample. I'm a big fan of GreenSock(GSAP), in fact with the lack of simple examples on how to transition routes I was already looking for a way to change the router history using code after an animation was completed. Since GSAP has a great set of callbacks my idea was to make the out animation and when that was completed update the route and create another animation (perhaps on componentDidMount
in order to animate in the other component, but with this sample I won't have to do that.
@VladShcherbin addEndListener
works with GSAP in a simple component transition. I made this simple transition using GSAP and the <Transition>
element, the code is like this:
import React, { Component } from 'react';
import Transition from 'react-transition-group/Transition';
const duration = 5000;
const defaultStyle = {
opacity: 0,
};
class Fade extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Transition
in={this.props.in} timeout={duration} mountOnEnter={true} unmountOnExit={true}
addEndListener={(n, done) => {
if (this.props.in) {
TweenLite.to(n, 1, { opacity: 1, onComplete: done });
} else {
TweenLite.to(n, 1, { opacity: 0, onComplete: done });
}
}}
>
{ state =>
<div className="card" style={{ marginTop: '10px', ...defaultStyle }}>
<div className="card-block">
<h1 className="text-center">FADE IN/OUT COMPONENT</h1>
</div>
</div>
}
</Transition>
);
}
}
The parent component looks like this:
import React, { Component } from 'react';
import Fade from './children';
class App extends Component {
constructor() {
super();
this.toggleEnterState = this.toggleEnterState.bind(this);
this.state = { isVisible: false };
}
toggleEnterState() {
const { isVisible } = this.state;
this.setState({ isVisible: !isVisible });
}
render() {
return (
<div className="col-12" style={{ marginTop: '10px' }}>
<button className="btn btn-primary" onClick={this.toggleEnterState}>
Toggle Component View
</button>
<Fade in={this.state.isVisible} />
</div>
);
}
}
In the live sample the duration is way bigger than the animation because if you want to create a timeline that could be longer, the component will be unmounted before the animation is complete.
https://codesandbox.io/s/yvYE9NNW
I haven't been able to make RR animations with GSAP, hopefully this sample could help me in getting it done.
Hopefully there could be some sort of cooperative work between RR4 and RTG in order to generate a consistent and simple way of animating route transitions.
I tried both CSSTransition and external library (animejs) for transition. With CSSTransition and css classes it's easy to create a transition, while with external library it's a lot harder (you have to deal with incomplete animations and other stuff).
It'll be interesting to see fluid page transition with external libraries (GSAP, animejs, etc). For my project, animating with CSSTransition is good enough.
Here is the result, there are simple (animated) and nested (non-animated) routes.
I'm closing the issue since it's solved for me, feel free to re-open it or continue discussion. Thank you for your help and ideas, @jquense @Horizon-Blue @rhernandog ! ❤️
@VladShcherbin Just completed a sample using GSAP, RTG and RR:
https://codesandbox.io/s/mQy3mMznn
At the end I couldn't use addEndListener
so I ended up using onEnter
and onExit
in order to make it work. Also to prevent incomplete animations the timeout prop passed to the <Transition>
element has to be equal to the animation duration defined in the GSAP instance so the animation can be completed before the element is unmounted from the DOM. Be aware that GSAP uses seconds and RTG milliseconds so be sure to make the conversion.
Also there are a few hacks that seem to be inevitable, such as setting the display to fixed and because of that I implemented a way to keep the width of the element equal to the parent's width and using clearProps
in the onComplete
callback, remove the style so the element can go back to it's natural size and normal position
value, if the screen is resized. Still is a lot of work but it gets the job done :wrench:
@rhernandog in your example, if you navigate between routes too fast, the components will be rendered on top of each other.
@VladShcherbin That can be expected, since the position:fixed
is removed when the animations are completed. You could create a boolean to prevent any other click event until the animation is completed.
My main concern is keeping the elements with position fixed permanently. This should be considered just as a proof of concept, it needs a lot of work, that's for sure.
Another possibility is not animate the element onExit
, that would prevent the overlapping issue.
@VladShcherbin I was playing with the sample and the issue is that if you click between routes too quick, the DOM element is not unmounted when a new animation is triggered. GSAP has a feature called overwrite manager that basically cancels an existing animation if a new one that affects the same DOM element and properties is created. In this case the previous instance that animates opacity and the x position is still running when a new one is created. The current active one is killed and sent to GC and the new one takes control over the DOM element.
Another choice could be use a fromTo
instance which forces the initial values of the element and then animates it to the final values, but that could create a lot of jumps in the animation, which ultimately is going to hurt the experience. I'd prefer creating a boolean and preventing any click event from changing the route.
@jquense @Horizon-Blue @rhernandog I found another unexpected bug 😞
Try clicking the same link many times and navigate back. The animation logic will be broken. This is because history
package in RR4 duplicates the same link in location history. It was working in RR3 though. I opened an issue for this.
Same issue exists here - http://animate.mhaagens.me. Try clicking the same link many times and navigate back. It will loop through duplicated locations and trigger animations every time.
Since we tried to remove nested route component re-animating with location.pathname
as a key, it's not working correctly for us. When you go back through duplicated locations, our component won't have any animations because location.pathname
is the same.
@VladShcherbin I see you landed on the issue I created some time ago :wink:
The solution I ended up using, that requires less effort than check the location key is to use hash history instead of browser history as the main router tag:
import {HashRouter as Router, Route} from "react-router-dom";
// then use it
const App = () =>
<Router>
// rest of your JSX
</Router>;
@rhernandog yes, but this is HashRouter
, I don't want hashes in my urls 😉
Oh, rollback to using location.key
as a key breaks nested routes like /articles/nested
because on every route change the top component will be re-animated which is not desired. This has to be solved somehow.
@Horizon-Blue any ideas?
@VladShcherbin How about this one: https://codesandbox.io/s/8nBON3Ej
As a workaround, I defined a custom "SafeLink
" wrapper that does the checking for Link
, which replaces (instead of pushes) the history when the current pathname is the same as the target. (The rule is controlled by the replace
props of Link
.)
@Horizon-Blue yeah, this seems like a nice workaround for now. It was actually working in RR v3 and history v3, but was changed in RR v4 and history v4.
We can track this issues (and related ones) to remove it later:
https://github.com/ReactTraining/history/issues/470, https://github.com/ReactTraining/history/issues/507
@VladShcherbin @Horizon-Blue @rhernandog First of all, I want to thank you guys because with this issue discussion I was able to understand a little bit more about the new React Transition Group.
However, I am still scratching my head on the onEnter, and onExit methods since these are not working for me and once fired both always trigger the same component instead of the one that is entering and the one that is leaving.
const currentKey = location.pathname.split('/')[1] || '/'
const timeout = { enter: 300, exit: 200 }
<TransitionGroup>
<Transition
key={currentKey}
timeout={timeout}
mountOnEnter={true}
unmountOnExit={true}
onEnter={(node)=> { console.log('enter', node); }}
onExit={(node) => { console.log('exit', node); }} >
<Switch location={location}>
<Route exact path="/cases/:caseURL" render={(props)=> {
return (!this.props.state.intro) ? (
<Case {...props} cases={this.props.state.cases} unlockBody={this.props.unlockBody} lockBody={this.props.lockBody} addCaseToState={this.props.addCaseToState}/>
) : null; }}/>
<Route exact path="/studio" render={(props)=> {
return (!this.props.state.intro) ? (
<Studio data={this.props.state.studio} {...props} />
) : null; }}/>
<Route exact path="/contact" render={(props)=> { return (!this.props.state.intro) ? (
<Contact data={this.props.state.contact} {...props} />
) : null; }}/>
</Switch>
</Transition>
</TransitionGroup>
My logging as soon as contact
route is triggered:
Hi, @bzin , can you verify that your location
is not undefined? I just try your setup, and the same problem occurs when I forgot to specify what is the location
.
You could take a look that this: https://codesandbox.io/s/k52zro4k3o . It is a little bit messy, but I hope it could give you a sense of what I am talking about.
@Horizon-Blue thanks a lot!
That was it! I was passing the window.location instead of a location
returned by the Route
.
In my setup I did not add a first <Route path="/" component={App} />
. I was doing everything in a Routing
component I created.
But now with this I am wondering how can we have different timeouts for different routes. Is this possible?
@bzin How about the method I mention here: https://github.com/ReactTraining/react-router/issues/5279#issuecomment-316877263 ?
I made a different kind of wrapper based on the original TransitionGroup
component to handle replacing one child with another mainly for transitioning between routes in mind. I also included a SwitchTransition
component for easy react-router
integration so you can use Transition
or CSSTransition
components to handle your animations. Check it out here: https://github.com/LKay/react-transition-replace Working examples are in the docs and storybook.
FWIW I'm using react-router 4 and react-router-redux
render() {
let location = this.props.router.location || {}
return (
<ConnectedRouter history = {this.props.history}>
<TransitionGroup>
<CSSTransition
key = {location.key}
classNames="fade"
timeout={2000}
appear= {true}
exit={false}
onEnter = {() => {
window.scrollTo(0, 0)
}}
>
<Switch location = {location}>
{ routes }
</Switch>
</CSSTransition>
</TransitionGroup>
</ConnectedRouter>
)
}
}
let mapStateToProps = state => {
let { router } = state
return {
router,
}
}
Routes = connect(mapStateToProps)(Routes)
export { Routes }
and here's the CSS:
.fade-appear,
.fade-enter {
opacity:0;
}
.fade-appear-active,
.fade-enter-active {
opacity:1;
transition: opacity 1.5s;
}
Do you know how to avoid rerender child component? Here I have an example https://codesandbox.io/s/jv98w12vo3 Home component renders 4 times on entering and 3 times on leave despite I use PureComponent.
Also, investigate optimization: https://reactjs.org/docs/optimizing-performance.html
I'm still running into the re-render issue in my project. A stripped down version of the code is below. We're creating an on-boarding wizard, hence the 'knownSteps" components. The re-render problem occurs when navigating between all routes, not just the steps, but I left it in there to better reflect the actual code.
const knownSteps = [
'step1',
'step2'
];
export const routes = (
<Route
path="/"
render={({ location }) => (
<Layout>
<TransitionGroup>
<CSSTransition key={location.key} classNames="fade" timeout={1000}>
<Switch location={location}>
<Route exact={true} path="/" component={Home} />
{knownSteps.map(s => (
<Route key={s} path={`/${s}`} component={ContentManager} />
))}
<Route path="/500" component={ServerError} />
<Route component={NotFound} />
</Switch>
</CSSTransition>
</TransitionGroup>
</Layout>
)}
/>
);
fwiw I ended up not using TransitionGroup and CSSTransition; that at least suppressed visible effects.
@totodot @afholderman Did you try using render
instead of component
? For example:
<Route exact path="/" render={() => <Home />}
https://reacttraining.com/react-router/web/api/Route/render-func
No I didn't. Thanks for the tip. This looks like an excellent resource!
@richmeij My issue ended up being related to how my components were accessing redux store data further down the chain. Freezing them in shouldComponentUpdate did the trick for me.
Just a follow-up to this discussion. FWIW, I added a piece of the documentation accompanied by a demo about usage with React Router. It will be deployed in the next release.
Basically you shouldn't wrap Route
or Switch
components with a TransitionGroup
, you should use CSSTransition
at the end of each route, like in the demo.
@silvenon Your documentation is really useful, thanks! However I'm getting errors with my default route (Warning: You tried to redirect to the same route you're currently on: "/users"
). Where I previously had (without transitions):
<Switch>
<Route path="/users">
<UsersPage />
</Route>
// More routes.
<Route render={() => (
<Redirect to="/users" />
)}></Route>
</Switch>
I now have (following the example in your documentation):
<Route path="/users">
{({ match }) => (
<CSSTransition in={match != null} timeout={500} classNames="fade" mountOnEnter={true} unmountOnExit={true}>
<UsersPage />
</CSSTransition>
)}
</Route>
// More routes...
<Route render={() => (
<Redirect to="/users" />
)}></Route>
Obviously, because the <Switch>
is gone, the redirect is being rendered every time, causing the above warning message. How can I only get the redirect to render if none of the other routes are matched without using a <Switch>
?
@DanielGibbsNZ An option could be to create an array of the paths and routes, just like @silvenon sample. With that you can check if the current location doesn't match any of them. You could use Lodash findIndex
method:
https://lodash.com/docs/4.17.10#findIndex
Something like this:
// routes array
const routes = [
{ path: '/', name: 'Home', Component: Home },
{ path: '/about', name: 'About', Component: About },
{ path: '/contact', name: 'Contact', Component: Contact },
];
// then inside your router add the following for the specific redirect
<Route path="/" render={({location}) => {
if( _.findIndex(routes, [path, location.pathname]) < 0){
return <Redirect to="/users" />;
} else {
return "";
}
}}
Maybe there's a better alternative, this is what comes to my mind now, hope it helps.
The inspiration is from this post - medium post.
tl;dr
Here is the result with React Router 4, CSSTransition and css classes - codesandbox. In order to change page transition you need to replace css properties. No external libraries are needed.
Initial issue:
Here is the result webpage from the article. It is working this way:
new
(animated on mount) andold
(animated on unmount)old
element has to be under thenew
element, which can be done with cssposition: fixed/absolute
. So, both elements render in the same spot.old
element is unmounting, fade-out animation with duration of 175ms is triggered and then element is removed from dom. The difference between 175ms and 250ms makes a nice transition effect.Since in v2 the API was changed, I'm trying to create similar transition effect. I do think this example will be helpful for other devs and I would like some help to make it work.
Switch
renders another component on location change [Solved]This is where I've come so far. Ideas and advices would be great - cc @jquense @Horizon-Blue
Update 1:
Switch
component is rendering the wrong component on exit because the current location is changed while the element is not removed from DOM yet. The solution is to passlocation
prop intoSwitch
in order to render the correct component during the whole lifecycle.Update 2
With CSSTransition and css classes it is relatively easy to create page transition once you understand the way it works. Here is the result - codesandbox.
For my project, this is good enough although it'll be interesting to see transition with external library (GSAP, animejs or another one).
Update 3
Found another bug with
history
package in RR4. Clicking the same link creates duplicates in location history and navigating back is broken iflocation.pathname
is used as a key (which was supposed to remove nested route animation). history issue, current workaround