Sliding Pages With React.js
Tags: react, javascript, web
Published: 2020-08-31 07:37:57

---
title: Sliding Pages With React.js
published: true
date: 2018-07-03 13:39:20 UTC
tags: ['react', 'javascript', 'web']
canonical_url: https://www.turtle-techies.com/sliding-pages-with-react-js/
---


## Make Them Slide!

The popular way to achieve routing in React is using <a href="https://github.com/ReactTraining/react-router" target="_blank">React Router v4</a>. React Router works great, but it doesn't make the pages slide. Of course, you can use a package like <a href="https://www.npmjs.com/package/react-router-page-transition" target="_blank">React Router Page Transition</a> to make that happen.  
We are not going to use either of that packages in this tutorial. Instead, we will roll our own and create a sliding pages router.  
This tutorial is for learning purposes, If you want to use the final code in production it would require some more work.  
The complete solution is on github and you can find it <a href="https://github.com/asafg6/react-sliding-pages" target="_blank">here</a>

## Prerequisites
You should be comfortable with HTML, Javascript, css and React.

## Step 1 - Setup
we'll use create-react-app. If you don't have React installed, <a href="https://reactjs.org/docs/add-react-to-a-new-app.html" target="_blank">install it</a>.  

Let's create our new app:
```shell
$ create-react-app react-sliding-pages
```
Alright! First step complete!
Feel free to delete logo.svg, we won't be using it.     
You can cd into the project's directory and run **npm start**, so you can watch your progress.




## Step 2 - Creating The Page Element

Create a new file called page.js under the src directory.  

**page.js**
```javascript

import React, { Component } from 'react';


class Page extends Component {

    constructor(props) {
        super(props);
    }


    render() {
        return (
            <div style={{
                width: '100vw', 
                height: '100%',
                position: 'fixed',
                top: 0,
                left: 0,
                transform: 'translateX('+ this.props.left + 'px)',
                animationTimingFunction: 'ease-in',
                zIndex: -20,
                transition: 'transform .8s ease-in-out'
            }}>
                {this.props.children}
            </div>
        );

    }

}

export default Page;


```

There's a few things to pay attention to in this component.  
First of all, this is a container component — we use **this.props.children** to render all child components.  
The other two are in the css:  
The **transformX** property is determined by the **left** prop.  
The transition and **animationTimingFunction** controls the animation and makes the sliding happen.

Now that we have our page component, we should try to use it.

## Step 3 - Sliding The Page

Let's take care of some crucial styling. Change the contents of App.css to this:

**App.css**
```css

.App {
  text-align: center;
}

.page {
  width: 300px;
  height: 100%;
  min-height: 5em;
  margin-right: auto;
  margin-left: auto;
  margin-top: 5em;
  padding: 2em;
  z-index: 15000;
}

```

Change the contents of App.js to:

**App.js**
```javascript

import React, { Component } from 'react';
import './App.css';
import Page from './page';

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {left: 0}
  }


  render () {
    return (
      <div className='App'>
        <div>
          <input type="text" onChange={(e) => this.setState({left: parseInt(e.target.value)})} />
        </div>
        <Page left={this.state.left}>
          <div className='page' style={{ backgroundColor: 'green' }}>
            bla1
          </div>
        </Page>
      </div>
    )
  }
}

export default App

```

Play around with the input, try typing 1000 or -500 and see what happens. If everything went well, the page should slide across the screen.  
So what did we do here?  
We created a state variable called **left** and passed it to our page component. When the input changes, it triggers the **setState** method and changes the page's left property.  
That's nice, but it's only one page. We need an element that will wrap our page components and switch them.

## Step 4 - Creating The Nav Element

This is the main part of our program and where the most logic is.
Let's create our Nav element in small parts.  

We'll start with an empty component. Create a new file called nav.js under the src directory.  
**nav.js**
```javascript

import React, { Component } from 'react';


class Nav extends Component {

}

export default Nav;
```

The plan is to place the current page in the middle of the screen and hide the other pages on the sides of the screen. To achieve that, we'll need to know the screen width.
The **left** prop worked well before, so we'll use it here as well.

Let's create the constructor for the Nav component:  

**nav.js**
```javascript

...

    constructor(props) {
        super(props);
        this.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
        let left = props.children.map(o => this.width);
        if (left.length > 0 ) {
            left[0] = 0;
        }
        this.state = {page: 0, left: left};        
    }

...

```

We now have the screen's width in **this.width**, an array of integers that represents the pages' left property and a page that represents the current page number.  

Now let's add the **move** method:  

**nav.js**
```javascript
...

    move(page) {
        const left = this.state.left.slice();
        if (page >= left.length) {
          page = 0;
        }
        for (let i = 0; i < left.length; i++ ) {
          if (i < page) {
            left[i] = -this.width;
          } else if (i === page) {
            left[i] = 0;
          } else {
            left[i] = this.width;
          }
        }    
        this.setState({left: left, page: page})
      }
      
...
```

Take a moment to read the code and understand what it does.  

Basically, the code iterates on a copy of the left array and sets each page's left location according to the page number.

Let's continue to the **render** method.  

**nav.js**  
```javascript
...

    render() {
        const pageElements = React.Children.map(this.props.children, (page, idx) =>
            React.cloneElement(page, { left: this.state.left[idx] })); 
        return (
            <div>
                {pageElements}
            </div>
        );
    }

...
```

Here, we use **React.Children.map** to iterate over the page elements and add the left prop.  

Now let's use our new Nav element in App.js.  

**App.js**
```javascript

import React, { Component } from 'react';
import './App.css';
import Page from './page';
import Nav from './nav';

class App extends Component {

  constructor(props) {
    super(props);
  }



  render() {
    return (
      <div className="App">
        <Nav>
          <Page>
            <div className="page" style={{ backgroundColor: 'green' }}>
              bla1
            </div>
          </Page>
          <Page>
            <div className="page" style={{ backgroundColor: 'red' }}>
              bla1
            </div>
          </Page>
          <Page>
            <div className="page" style={{ backgroundColor: 'blue' }}>
              bla1
            </div>
          </Page>
          <Page>
            <div className="page" style={{ backgroundColor: 'brown' }}>
              bla1
            </div>
          </Page>
        </Nav>
        
      </div>
    );
  }
}

export default App;


```

In case everything worked well, you should see the first page rendered in your browser. You can check the html source (using the browser's dev tools) and see that all the child elements are indeed rendered.  

Alright, so we have all the pages but no access to the **move** method. Let's add some buttons temporarily just to see that the slide works.  

**nav.js**
```javascript
...

    render() {
        const pageElements = React.Children.map(this.props.children, (page, idx) =>
            React.cloneElement(page, { left: this.state.left[idx] })); 
        const buttonElements = React.Children.map(this.props.buttons, (button, idx) => {
            let newButton = React.cloneElement(button , { onClick: () => this.move(idx), ...button.props});
            return newButton;
        });
        return (
            <div>
                <div>
                    {buttonElements}
                </div>
                <div>
                    {pageElements}
                </div>
            </div>
        );
    }

...
```

**App.js**
```javascript
...

  render() {
    let buttons = [];
    for (let i = 0; i < 4; i++) {
      let button = <button key={i.toString()}>{i}</button>;
      buttons.push(button);
    }
    return (
      <div className="App">
        <Nav buttons={buttons}>
          <Page>
            <div className="page" style={{ backgroundColor: 'green' }}>
              bla1
            </div>
          </Page>
          <Page>
            <div className="page" style={{ backgroundColor: 'red' }}>
              bla1
            </div>
          </Page>
          <Page>
            <div className="page" style={{ backgroundColor: 'blue' }}>
              bla1
            </div>
          </Page>
          <Page>
            <div className="page" style={{ backgroundColor: 'brown' }}>
              bla1
            </div>
          </Page>
        </Nav>
        
      </div>
    );
  }

...
```

Try the buttons in your browser, the pages should slide.  

Now that the basic functionality works, let's make our Nav element more router like.  

## Step 5 - Implement Routing

First, remove the buttons we used for testing. We don't need them.
With that out of the way, create a new file called link.js under the src directory.

**link.js**
```javascript

import React, { Component } from 'react';


class Link extends Component {

    go() {
        window.location.hash = this.props.to.slice(1);
    }

    render() {
        return (
            <i onClick={() => this.go()}>
                {this.props.children}
            </i>

        );
    }

}

export default Link;

```

This component is pretty straight forward, it renders the component's children and changes the hash on a click according to the **to** prop.

Let's make our Nav component react to hash changes.

**nav.js**
```javascript
...

    constructor(props) {
        super(props);
        this.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
        let left = props.children.map(o => this.width);
        if (left.length > 0 ) {
            left[0] = 0;
        }
        this.state = {page: 0, left: left, from: left};
        this.pages = {};
    }

...

    componentDidMount() {
        this.move(this.pages[window.location.hash.slice(1)]);
    }

    componentWillMount() {
        window.onhashchange = (e) => {
            const i = e.newURL.indexOf('#');
            const hash = e.newURL.slice(i + 1);
            this.move(this.pages[hash]);
        };
    }


    render() {
        const pageElements = React.Children.map(this.props.children, (page, idx) => {
            const element = React.cloneElement(page, { left: this.state.left[idx], from: this.state.from[idx], eventer:this.eventer }); 
            this.pages[element.props.path.slice(1)] = idx;
            return element;
        });
        return (
            <div>
                {pageElements}
            </div>
        );
    }

...
```

As you can see we, are registering the pages to the **pages** prop, so we can know which route belongs to which page.

And finally use our new links in App.js.

**App.js**
```javascript

import React, { Component } from 'react';
import './App.css';
import Page from './page';
import Nav from './nav';
import Link from './link';

class App extends Component {

  render() {
    return (
      <div className="App">
        <div>
          <Link to="/green" >
            <button>green</button>
          </Link>
          <Link to="/red" >
            <button>red</button>
          </Link>
          <Link to="/blue" >
            <button>blue</button>
          </Link>
          <Link to="/brown" >
            <button>brown</button>
          </Link>
        </div>
        <Nav>
          <Page path="/green">
            <div className="page" style={{ backgroundColor: 'green' }}>
              bla1
            </div>
          </Page>
          <Page path="/red">
            <div className="page" style={{ backgroundColor: 'red' }}>
              bla1
            </div>
          </Page>
          <Page path="/blue">
            <div className="page" style={{ backgroundColor: 'blue' }}>
              bla1
            </div>
          </Page>
          <Page path="/brown">
            <div className="page" style={{ backgroundColor: 'brown' }}>
              bla1
            </div>
          </Page>
        </Nav>
        
      </div>
    );
  }
}

export default App;

```

Alright! We have a sliding router! But wait...  
The pages that are off screen are still rendered. This might be good for you, if your app is pretty much static. But as we know, React components might do a whole lot of stuff.. Use the the network, do some calculations, animations... etc. We need some way to disable the component that are off screen.

## Step 6 - Only Render What Is Visible

This part is a bit tricky, we need the pages visible for the sliding effect but we don't want them to be rendered when they are off screen. For example, if the user goes from page 5 to page 1, we need to render all the pages for the sliding effect and then only render page 1.  

We'll use an event mechanism, that will fire every time the page changes.  
Add another class to nav.js .  

**nav.js**
```javascript
...

class MoveEvent {

    constructor() {
        this.listeners = [];
    }

    register(f) {
        this.listeners.push(f);
    }

    unregister(f) {
        this.listeners.splice(this.listeners.indexOf(f), 1);
    }

    fire(params) {
        this.listeners.forEach((listener) => {
            listener(params);
        });

    }
}


...
```

This is a pretty standard event class. We will also add a **from** prop, so that the page element will be able to tell if it's in transition or not.
Let's pass the props to our page components from our Nav component.

**nav.js**
```javascript
...

class Nav extends Component {

    constructor(props) {
        super(props);
        this.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
        let left = props.children.map(o => this.width);
        if (left.length > 0 ) {
            left[0] = 0;
        }
        this.state = {page: 0, left: left, from: left};
        this.pages = {};
        this.eventer = new MoveEvent();
    }

    move(page) {
        const left = this.state.left.slice();
        const from = left.slice();
        if (page >= left.length) {
          page = 0;
        }
        for (let i = 0; i < left.length; i++ ) {
          if (i < page) {
            left[i] = -this.width;
          } else if (i === page) {
            left[i] = 0;
          } else {
            left[i] = this.width;
          }
        }    
        this.setState({left: left, page: page, from: from});
        this.eventer.fire();
      }
      
    componentDidMount() {
        this.move(this.pages[window.location.hash.slice(1)]);
    }

    componentWillMount() {
        window.onhashchange = (e) => {
            const i = e.newURL.indexOf('#');
            const hash = e.newURL.slice(i + 1);
            this.move(this.pages[hash]);
            this.eventer.fire();
        };
    }


    render() {
        const pageElements = React.Children.map(this.props.children, (page, idx) => {
            const element = React.cloneElement(page, { left: this.state.left[idx], from: this.state.from[idx], eventer:this.eventer }); 
            this.pages[element.props.path.slice(1)] = idx;
            return element;
        });
        return (
            <div>
                {pageElements}
            </div>
        );
    }

}


...
```

Great, Now let's make the page understand if it needs to render children or not.


**page.js**
```javascript

import React, { Component } from 'react';

function sleep(ms) { // just a sleep util
    return new Promise(resolve => setTimeout(resolve, ms));
}

class Page extends Component {

    constructor(props) {
        super(props);
        this.state = {shouldRenderChildren: true};
    }

    isVisible() {
        if (this.props.left < 0) {
            return false;
        }
        if (this.props.left >= window.innerWidth) {
            return false;
        }
        return true;
    }

    move() {
        if (this.props.left !== this.props.from) {
            this.setState({ shouldRenderChildren: true });
        }
        this.reRender();
    }

    async reRender() {
        await sleep(1000);
        this.setState({shouldRenderChildren: this.isVisible()});
    }

    render() {
        return (
            <div
             style={{
                width: '100vw', 
                height: '100%',
                position: 'fixed',
                top: 0,
                left: 0,
                transform: 'translateX('+ this.props.left + 'px)',
                animationTimingFunction: 'ease-in',
                zIndex: -20,
                transition: 'transform .8s ease-in-out'
            }}> {this.state.shouldRenderChildren &&
                    this.props.children
                 }
            </div>
        );

    }

}

export default Page;

...

```

The **isVisible** method returns whether or not the page is off screen. The **reRender** method sleeps for 1 second and updates the **shouldRenderChildren** state variable. When move is called the element will be fully rendered for 1 second and then it will disappear if it's off screen.  

Now we just need to register to the move event.

**page.js**
```javascript
...

    componentDidMount() {
        this.props.eventer.register(() => this.move());
    }

    componentWillUnmount() {
        this.props.eventer.unregister(() => this.move());
    }

...

```

Awesome! Check your browser dev tools and see that off screen pages are not rendering any children. 

## Step 7 (optional) - Customizing And Extending The Code

There's a chance that your app needs the pages to behave differently.  
For example, you can render each page's child components and pass them a prop telling them if they are visible or not. Whatever comes to mind really.  
That part is totally up to you!  
If you are extending or customizing the code, I would love to hear about your ideas and implementations.


## Summary

We created our own router with sliding pages, that's pretty cool. Of course our router is no match for React Router, which is much more fancy and also battle tested. Rolling your own can be a huge pain sometimes(no one likes reinventing the wheel), but it has a big advantage - you can code exactly what your app needs.  
I hope you enjoyed, and of course, feel free to comment or share.  
Thank you for reading.