-1

This is a question about React router and its design, specially when considering pages transitions.

React is designed for modularity, and it excels on that area. But I found one task that seems pretty hard to do while maintaining isolation and modularity between the components: pages transitions.

I did read many articles and questions related to this, both in Medium and StackOverFlow among other sites; but up to the date I didn't read any description of a good architectural approach to solve the issues I will address here, and an explanation about why React router is not considering this common cases.


So lets consider a site with three pages, PageA, PageB and PageC:

If we want to use the same animation for mounting/unmounting these three pages, lets say a FadeIn for mounting and a FadeOut for unmounting, then we can use the React Transition Group component to wrap our <Switch> component, and it will be in charge of animating the pages using CSS transitions. Here the CSS transitions will be controlled by <CSSTransition> component with the class passed to ClassNames.

<div className="Main">
    <TransitionGroup component={null}>
        <CSSTransition
            key={locationKey}
            appear={true}
            classNames="Main"
            timeout={{
                enter: 300,
                exit: 300,
            }}
        >
            <Switch location={this.props.location}>
                <Route
                    path="/PageA"
                    render={(props) => {
                        return <PageA />;
                    }}
                />
                <Route
                    path="/PageB"
                    render={(props) => {
                        return <PageA />;
                    }}
                />
                <Route
                    path="/PageC"
                    render={(props) => {
                        return <PageA />;
                    }}
                />
            </Switch>
        </CSSTransition>
    </TransitionGroup>
</div>

Everything all right, we are happy and our site has some nice animations, both for mounting and unmounting.

Now we want to make different transitions for each mounting/unmounting action for each page: PageA to mount with FadeIn and unmount with a FadeOut, PageB mount with a slide from left to right and unmount sliding back to the left, and PageC to mount from bottom to top and unmount sliding back to bottom.

We will face two problems: if we use <CSSTransition> component, we will have to pass different classNames prop to it depending on the page rendered. I didn't implemented anything with this strategy yet, because there is another issue that concerns me: if follow this path, we will store the logic for animating our page components inside <CSSTransition> component, and this is outside the pages. This doesn't look very modular and isolated to me, and the more books I read about software design, modularity and isolation, the more confused I am with this approach.

In an ideal world the logic for page animations should be defined in the main component of each page. Something like:

class Home extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            classForAnimation: '',
        };
    }

    componentDidMount() {
        this.setState({
            classForAnimation: ' FadeIn',
        });
    }

    render() {
        return <div className={'Home' + this.state.classForAnimation}>
                   I'm Home page
               </div>;
    }
}

export default Home;

And some CSS:

.Home {
    opacity: 0;
    transition: opacity 0.5s ease;
}

.FadeIn {
    opacity: 1;
}

This is only for mounting, really easy. So, if this is a better solution, ¿why we don't use it?

The reason is because React-router don't know about our transitions, and it will unmount the page to animate out immediately, without even waiting the animation to start —and, of course, to finish—. This is what <CSSTransition> component does, but concentrating the logic of pages transitions outside the page components.

So, considering the case of multiple pages with different animations on mount/unmount, here are my questions:

  • A) Is there any reason why the logic for the transitions between pages shouldn't be stored inside the main component of each page?

  • B) Is there any reason why React-router were designed to unmount components ignoring the case of waiting for different type of events in different pages?

  • C) Is there a common approach to solve this issue that I am not considering?

  • D) Writing a custom react router from scratch would be an option?

Nik.

2 Answers2

0

You can use the Link component here that let you manage the lifecycle:

https://yeutech-lab.github.io/react-router-dom-utils/#link

Installation:

npm install @yeutech-lab/react-router-dom-utils --save

Usage:

export default (
  <Link
   component={(props) => <div {...props}>Hello world</div>}
   waitChunk={true}
   // You can pass routes as props if you use waitChunk
   routes={[{ name: 'Home', component: Home, path: '/' }]}
   // OR a ContextConsumer that own it
   ContextConsumer={AppContextConsumer}
   // use lifecycle callbacks
   onClick={actionStartLoadingIndicator}
   onPreload={() => console.log(`
     chunk load in the background on mouseover or
     when user click if waitChunk is true
   `)}
   onLoaded={actionStopLoadingIndicator}
   // use delay function so you can animate what you want
   delay={() => Math.random() * 1000}
  />
);

This component support life cycle functions for react router so you can handme your transition logic. It doesn't rely on any additional lib.

Handy features for preloading the pages chunks using react-loadable and on mouse hover is also available.

You can even use preload chunk when the browser is idle with webpack.

Page transition are supported, you can even control a nav top loading indicator for your page transition.

I had the same issue and needed more flexibility so I did this.

Edit

About your 4 questions:

A) Is there any reason why the logic for the transitions between pages shouldn't be stored inside the main component of each page?

Yes, when a page change happen in react-router, it unmount the current page and render the next one.

If you want a transition, no matter what, you're previous page will already be unmounted.

B) Is there any reason why React-router were designed to unmount components ignoring the case of waiting for different type of events in different pages?

Yes, because react-router is here for providing a logic for playing with location and history to create webapp.

It does it very well.

C) Is there a common approach to solve this issue that I am not considering?

Yes, you must use withRouter and manage the transition using this API.

D) Writing a custom react router from scratch would be an option?

That sound like a great idea if you believe react-router does not help in your case. Please share your work on GitHub when it's done.

Best

Dimitri Kopriwa
  • 13,139
  • 27
  • 98
  • 204
  • Thanks @DimitriKopriwa, your component looks interesting, I will have a look to it. But you didn't address any of my four questions! and I'm pretty sure that you know something about that… ;) –  Jan 20 '19 at 21:30
  • Thanks @DimitriKopriwa! A) Yes, but then why react-router wasn't designed to listen events inside the component that will unmount? B) Yes, it does… but the lack of a listening logic is driving crazy to the developers that doesn't fit in a common easy case. C) With `withRouter`… I will dig into it :) –  Jan 21 '19 at 08:24
  • A) They don't need to. This is why `withRouter` is here. B) you don't have to use their link component, use `withRouter`. C) You're on the right track. Is this valid answer? – Dimitri Kopriwa Jan 21 '19 at 15:25
0

react-route-transition is a simple library I made that helps implement transition animations when using react-router.

Simply wrap your app with the provided Provider and use the useTransition hook within your components to describe their enter and leave animations.

For example, the following will start the onEnter animation as the route changes to "/" (after firing the onLeave animation(s) of the previous route) and will start the onLeave animation as the route changes from "/", right before unmounting the component (by changing the route) and firing the next component's onEnter animation.

useTransition({
  handlers: [
    {
      path: '/',
      onEnter: async () => {
        await gsap
          .timeline()
          .fromTo(
            '[data-home-main] > *, [data-home-footer]',
            { opacity: 0, y: 20 },
            { duration: 0.6, stagger: 0.125, y: 0, opacity: 1 }
          );
      },
      onLeave: async () => {
        await gsap.timeline().to('[data-home-main] > *, [data-home-footer]', {
          duration: 0.6,
          stagger: 0.125,
          opacity: 0,
          y: -20,
        });
      },
    },
  ],
});
dutzi
  • 1,880
  • 1
  • 18
  • 23