page sliding animation with React Router v4 and react-transition-group v2

12.1k views Asked by At

I'm using React Router v4 and react-transition-group v2 to test the page sliding animation.

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:

enter image description here

As you see, the animation that index page slide to the detail page is all right(right to left). 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.


Update: 2017.08.31

Thanks for @MatijaG, using the path depth is really an awesome idea. I followed it and got a new problem.

function getPathDepth(location) {
  let pathArr = (location || {}).pathname.split('/');
  pathArr = pathArr.filter(n => n !== '');
  return pathArr.length;
}

<Route render={({ location }) =>
  <TransitionGroup>
    <CSSTransition
      key={location.pathname.split('/')[1]}
      timeout={500}
      classNames={ getPathDepth(location) - this.state.prevDepth > 0 ? 'pageSliderLeft' : 'pageSliderRight' }
      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/lol" component={ LOL } /> // add a new route
        <Route path="/games" component={ Games } />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
} />

And updated CSS:

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

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

The animation:

enter image description here

From '/' to '/games' is ok, and from '/games' back to '/' is still ok(type 1: route A -> route B, only 2 routes). But if firstly from '/' to '/games', and then from '/games' to '/games/lol', the second phase lose the animation(type 2: route A -> route B -> route C, 3 or more routes). We also see that from '/games/lol' back to '/games' and then back to '/', the slide animation is not same as type 1.

Anyone has any idea about this problem?

4

There are 4 answers

1
MatijaG On BEST ANSWER

Well the main problem is that even in your question you did not specify how should the app know which direction to use.

One way you could do it is by using the path depth/length: if the path you are navigating to is "deeper" than the current path, transition right to left, but if its shallower transition from left right?

The other problem is that router only gives you the location you are going to, so you should probably save the previous location (or depth) so you can compare it to.

After that is just a matter of switching between css classnames, something along this lines:

import React from 'common/react'
import { withRouter, Route, Switch } from 'react-router'

function getPathDepth (location) {
    return (location || {} ).pathname.split('/').length
}
class RouterMap extends React.PureComponent {
    constructor (props, context) {
        super(props, context)
        this.state = {
            prevDepth: getPathDepth(props.location),
        }
    }

    componentWillReceiveProps () {
        this.setState({ prevDepth: getPathDepth(this.props.location) })
    }
    render () {

        return (
            <Router>
                <Route render={ ({ location }) =>
                    (<TransitionGroup>
                        <CSSTransition
                            key={ location.pathname.split('/')[1] }
                            timeout={ 500 }
                            classNames={ getPathDepth(location) - this.state.prevDepth ? 'pageSliderLeft' : 'pageSliderRight' } 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>
        )
    }
}

export default withRouter(RouterMap)

Codepen: https://codepen.io/matijag/pen/ayXpGr?editors=0110

0
kuubson On

I'm bringing here simplified and "hooked" version of @Darbley answer which works reliable

App.js

import React, { useEffect, useState } from 'react'
import styled from 'styled-components/macro'
import { hot } from 'react-hot-loader/root'
import { setConfig } from 'react-hot-loader'
import { Switch, Route } from 'react-router-dom'
import { TransitionGroup, CSSTransition } from 'react-transition-group'

import { compose } from 'redux'
import { withRouter } from '@hoc'

import Home from '@components/Home/Home'
import About from './About/About'
import Contact from './Contact/Contact'

import Navbar from './Navbar/Navbar'

setConfig({
    reloadHooks: false
})

const AppContainer = styled.main``

const pages = [
    { path: '/', order: 1 },
    { path: '/about', order: 2 },
    { path: '/contact', order: 3 }
]

const App = ({ location }) => {
    const [pageDirection, setPageDirection] = useState()
    const [currentPath, setCurrentPath] = useState(location.pathname)
    const [currentPathOrder, setCurrentPathOrder] = useState(
        pages.filter(({ path }) => path === location.pathname)[0].order
    )
    const currentKey = location.pathname.split('/')[1] || '/'
    useEffect(() => {
        const newPath = location.pathname
        const newPathOrder = pages.filter(({ path }) => path === newPath)[0].order
        if (newPath !== currentPath) {
            const direction = currentPathOrder < newPathOrder ? 'left' : 'right'
            setCurrentPath(newPath)
            setCurrentPathOrder(newPathOrder)
            setPageDirection(direction)
        }
    })
    return (
        <AppContainer>
            <Navbar />
            <TransitionGroup className={`${pageDirection}`}>
                <CSSTransition key={currentKey} timeout={1000} classNames={'route'}>
                    <div className="route__container">
                        <Switch location={location}>
                            <Route path="/" exact component={Home} />
                            <Route path="/about" component={About} />
                            <Route path="/contact" component={Contact} />
                        </Switch>
                    </div>
                </CSSTransition>
            </TransitionGroup>
        </AppContainer>
    )
}

export default process.env.NODE_ENV === 'development'
    ? compose(withRouter)(hot(App))
    : compose(withRouter)(App)

index.scss

.route__container {
    width: 100%;
    height: 100vh;
    position: absolute;
    top: 0px;
    left: 0px;
    transition: 1s;
}

.left .route-enter {
    transform: translateX(100%);
}

.left .route-enter-active {
    transform: translateX(0%);
}

.left .route-exit {
    transform: translateX(-100%);
}

.left .route-exit-active {
    transform: translateX(-100%);
}

.right .route-enter {
    transform: translateX(-100%);
}

.right .route-enter-active {
    transform: translateX(0%);
}

.right .route-exit {
    transform: translateX(100%);
}

.right .route-exit-active {
    transform: translateX(100%);
}
0
Darbley On

Expanded answer to accommodate more pages and keep left and right transitions in order.

import React from 'react';
import './assets/scss/styles.scss';
import { Route, Switch, withRouter  } from "react-router-dom";
import {TransitionGroup, CSSTransition } from 'react-transition-group';
import Header from './components/Header';
import Home from './pages/Home';
import Yellow from './pages/Yellow';
import Red from './pages/Red';
import Green from './pages/Green';
import Blue from './pages/Blue';
import Purple from './pages/Purple';
const pages = [
    { path: '/', name: 'home', order: 1 },
    { path: '/yellow', name: 'yellow', order: 2 },
    { path: '/red', name: 'red', order: 3 },
    { path: '/green', name: 'green', order: 4 },
    { path: '/blue', name: 'blue', order: 5 },
    { path: '/purple', name: 'purple', order: 6 }
]

class App extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            currentPage: this.setPage(this.props.location.pathname),
            curPageOrder: this.setCurrentOrder(this.props.location.pathname),
            newPageOrder: null
        }
    }
    componentDidUpdate(prevProps, prevState){
        console.log('Component did update');

        let newPage = this.setPage(this.props.location.pathname);
        let newPageOrder = pages.filter(function (page) {
            return page.name === newPage;
        });

        let curPage = this.state.currentPage;
        let curPageOrder = pages.filter(function (page) {
            return page.name === curPage;
        });

        if( newPage !== curPage){
            console.log('new page');
            let direction = curPageOrder[0].order < newPageOrder[0].order ? 'left' : 'right';
            // Set State
            this.setState({
                currentPage: newPage,
                pageDirection: direction,
                curPageOrder: curPageOrder[0].order,
                newPageOrder: newPageOrder[0].order,
            })

        }

    }
    setCurrentOrder = (path) => {
        let curPageOrder = pages.filter(function (page) {
            return page.path === path;
        });

        return curPageOrder[0].order;
    }

    setPage = (pathname) => {
        // SET PAGE FOR CSS CLASSES
        let page = null;
        switch (pathname){
            case('/'):
                page = 'home';
                break;
            case('/yellow'):
                page = 'yellow';
                break;
            case('/red'):
                page = 'red';
                break;
            case('/green'):
                page = 'green';
                break;
            case('/blue'):
                page = 'blue'
                break;
            case('/purple'):
                page = 'purple';
                break;
            default:
                page = 'home';

        }

        return page;
    }
    render() {
        const { location } = this.props;
        const currentKey = location.pathname.split("/")[1] || "/";


        return (
            <div className={`wrapper ${this.setPage(this.props.location.pathname)}`}>
                <Header />
                <div className={`wrap ${currentKey} `}>
                    <TransitionGroup  className={`transition-group ${this.state.pageDirection}`}>
                        <CSSTransition
                            key={currentKey}
                            timeout={{ enter: 800, exit: 400 }}
                            classNames={'transition-wrap'}

                        >

                            <section className={`route-section fade`}>
                                <Switch location={location}>
                                    <Route exact path="/" component={() => <Home />} />
                                    <Route path="/yellow" component={() => <Yellow /> } />
                                    <Route path="/red" component={() => <Red /> } />
                                    <Route path="/green" component={() => <Green /> } />
                                    <Route path="/blue" component={() => <Blue /> } />
                                    <Route path="/purple" component={() => <Purple /> } />   
                                </Switch>
                            </section>

                        </CSSTransition>
                    </TransitionGroup>
                </div>
            </div>
        )
    }
}
export default withRouter(App);

https://codesandbox.io/s/github/darbley/react-page-slide-transitions

0
Kevin Williams On

I wrote an AnimatedSwitch for react router v4 native.

https://javascriptrambling.blogspot.com/2020/07/react-router-native-animatedswitch.html

I used the history.action to determine if i was going forward or backward (ie. slide in or slide out). Then just made sure my transitions used the appropriate action to match the animation I wanted. Use history.goBack or history.replace to slide it out. Normal history.push or will slide it in.

      if (history.action === 'POP' || history.action === 'REPLACE') {
        setNewAnimationStyle(animationType.backNew(anim));
        setPrevAnimationStyle(animationType.backPrevious(anim));
      } else {
        setNewAnimationStyle(animationType.new(anim));
        setPrevAnimationStyle(animationType.previous(anim));
      }