Building a modal module for React with React-Router
Tags: react, javascript, webdev
Published: 2019-10-21 17:04:57

---
title: Building a modal module for React with React-Router
published: true
date: 2019-10-21 16:00:54 UTC
tags: react,javascript,webdev
canonical_url: https://blog.logrocket.com/building-a-modal-module-for-react-with-react-router/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/4fbbgnnrllwlawidya8o.png
---
**Written by [Doğacan Bilgili](https://blog.logrocket.com/author/dbilgili/)**✏️

Modals are very useful for displaying one view on top of another.

However, they are more than an absolutely positioned `<div>` element wrapping everything when it comes to implementation. Especially if you need dynamic URLs, page refreshes, or a simple scrolling interaction on a mobile device.

In this article, we’ll discuss the various aspects of modals and identify solutions to satisfy the requirements that come with creating dynamic URLs, page refreshes, and other features.

Before starting to shape the modal component, let’s start with some basics of the **react-router** package.

We’ll use four components from this package: BrowserRouter, Route, Link, and Switch.

Since this is not a **react-router** tutorial, I won’t be explaining what each of these components do.

However, if you’d like some info about **react-router** , you can check out [this](https://reacttraining.com/react-router/web/guides/quick-start) page.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

### Basic Routing

First, go ahead and install `react-router-dom` through npm.

```jsx
npm install react-router-dom --save
```

At the very top level of your application, use the `<BrowserRouter/>` component to wrap your app.

```jsx
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);
```

Inside `<App/>`, you’ll need to specify the routes so that you can render a specific view when one of them — or none of them — match.

Let’s assume we have three different components to render: `<Home/>`, `<About/>` and `<Contact/>`. We’ll create a navigation menu, which will always be visible at the very top of the application.

The `<Link/>` or `<NavLink/>` components from `react-router-dom` are used for navigation purposes, while `<NavLink/>` has the special feature of being applicable to a specific styling when the current URL matches.

Functionality-wise, you can use either one.

Below is the basic structure of the navigation menu, which changes the URL accordingly:

```jsx
render() {
  return (
    <div className="app">
      <div className="menu">
        <Link className="link" to='/'>Home</Link>
        <Link className="link" to='/about'>About</Link>
        <Link className="link" to='/contact'>Contact</Link>
      </div>
    </div>
  );
}
```

The next thing we’ll do is implement the mechanism that matches the URL and renders a specific component.

`<Switch/>` renders the first matching location specified by its `<Route/>` children. When nothing is matched, the last `<Route/>` is returned — usually as a 404 page.

```jsx
render() {
  return (
    <div className="app">
      <div className="menu">
        <Link className="link" to='/'>Home</Link>
        <Link className="link" to='/about'>About</Link>
        <Link className="link" to='/contact'>Contact</Link>
      </div>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/contact/" component={Contact} />
        <Route exact path="/about" component={About} />
        <Route>{'404'}</Route>
      </Switch>
    </div>
  );
}
```

### Creating a Modal Component

So far, we’ve implemented the basic routing structure. Now we can create a modal component and work on displaying it as an overlay.

Although there are a variety of different methods for creating modal components, we’ll only be covering one.

A modal component has a wrapper element which spans the whole screen — width and height.

This area also acts as a `clickedOutside` detector. Then the actual modal element is positioned relative to that wrapper element.

![An example of a modal router element.](https://blog.logrocket.com/wp-content/uploads/2019/10/Modal-router-element-nocdn.png)

Below is an example of a `<Modal/>` functional component using `withRouter` HOC (Higher order component) to access the router history and call the `goBack()` method to change the application URL when the modal is closed on click to `.modal-wrapper`.

`onClick={e => e.stopPropagation()}` is used to prevent propagation of the click event and trigger the `onClick` on `.modal-wrapper`, which would close the modal when the actual `.modal` element is activated.

```jsx
import React from 'react';
import { withRouter } from 'react-router-dom';

const Modal = () => (
  <div
    role="button"
    className="modal-wrapper"
    onClick={() => this.props.history.goBack()}
  >
    <div
      role="button"
      className="modal"
      onClick={e => e.stopPropagation()}
    >
      <p>
        CONTENT
      </p>
    </div>
  </div>
);

export default withRouter(Modal);
```

Styling the `.modal-wrapper` is just as important. Below, you can find the basic styling used to make it span the whole screen and appear above the content.

Using `-webkit-overflow-scrolling: touch` enables elastic scroll on iOS devices.

```jsx
.modal-wrapper {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100vh;
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}
```

### Opening the Modal View

The modal component we created should render on top of the existing view when a specific URL is matched, meaning that somehow we have to change the URL so the routing mechanism can decide what to render.

We know that `<Switch/>` renders the first matching location, but a modal overlay needs two `<Route/>` components rendering at the same time.

This can be achieved by putting the modal `<Route/>` out of `<Switch/>` and rendering it conditionally.

In this case, we should be able to detect if a modal is active or not.

The easiest way to do this is by passing a state variable along with a `<Link/>` component.

In the same way we used the `<Link/>` component to create the navigation menu, we’ll use it to trigger a modal view.

The usage shown below lets us define a state variable, which is then made available in the `location` prop, which we can access within any component using `withRouter` HOC.

```jsx
<Link
  to={{
    pathname: '/modal/1',
    state: { modal: true }
  }}
>
  Open Modal
</Link>
```

Put this anywhere you want. Clicking the link will change the URL to `/modal/1`.

There might be several modals with different names like `modal/1`, `modal/2`, and so on.

In this case, you’re not expected to define each `<Route/>` intended to match the individual modal locations. In order to handle all of them under the `/modal` route, use the following syntax:

```jsx
<Route exact path="/modal/:id">
```

This gives you the flexibility of getting the value of the hardcoded `:id` parameter within the modal component through the `match.params` prop.

It also lets you do dynamic content renderings, depending on which modal is open.

### Matching the Modal Location

This section is particularly important because it identifies the mechanism for displaying a modal on top of an existing view even though the location parameter changes when a modal is opened.

When we click the `Open Modal` link defined in the previous section, it will change the location path to `/modal/1`, which matches nothing in `<Switch/>`.

So we have to define the following `<Route/>` somewhere.

```jsx
<Route exact path="/modal/:id" component={Modal} />
```

We want to display the `<Modal/>` component as an overlay.

However, putting it inside `<Switch/>` would match it and only render the `<Modal/>` component. As a result, there would be no overlay.

To resolve this problem, we need to define it both inside and outside of `<Switch/>` with extra conditions.

* * * 

Below, you’ll see the modified version of the same snippet. There are several changes. Let’s list them quickly:

- There is a `previousLocation` variable defined in the constructor.

- There is an `isModal` variable defined, which depends on some other values.

- `<Switch/>` is using a `location` prop.

- There are two `<Route` _`exact`_`path="/modal/:id" component={Modal} />` used both inside and outside `<Switch/>`, and the one outside is conditionally rendered.

When a modal is opened, we need to store the previous location object and pass this to `<Switch/>` instead of letting it use the current location object by default.

This basically tricks `<Switch/>` into thinking it’s still on the previous location — for example `/` — even though the location changes to `/modal/1` after the modal is opened.

This can be achieved by setting the `location` prop on `<Switch/>`.

The following snippet replaces the `previousLocation` with the current location object when there is no open modal.

When you open a modal, it doesn’t modify the `previousLocation`.

As a result, we can pass it to `<Switch/>` to make it think we’re still on the same location, even though we changed the location by opening a modal.

We know that when a modal is opened, the state variable named `modal` in the `location` object will be set to `true`.

We can check if the state of the location object is defined and has the state variable of `modal` set to `true`.

However, these two checks alone do not suffice in the case of refreshing the page.

While the modal has to be closed on its own, `location.state && location.state.modal` still holds.

Checking whether `this.previousLocation !== location`, we can make sure that refreshing the page will not result in setting `isModal` to `true`.

When the modal route is visited directly, which is `modal/1` in our example, then none of the checks are `true`.

Now we can use this boolean value to both render the `<Route/>` outside of the `<Switch/>`, and to decide which `location` object to pass to `location` prop of `<Switch/>`.

Given that `<Modal/>` component has the necessary stylings, this results in two different views rendering on top of each other.

```jsx
constructor(props){
  super(props);
  this.previousLocation = this.props.location;
}

componentWillUpdate() {
  const { location } = this.props;
  if (!(location.state && location.state.modal)) {
    this.previousLocation = this.props.location;
  }
}  

render() {
  const { location } = this.props;
  const isModal = (
    location.state &&
    location.state.modal &&
    this.previousLocation !== location
  );

  return (
    <div className="app">
      <div className="menu">
        <Link className="link" to='/'>Home</Link>
        <Link className="link" to='/about'>About</Link>
        <Link className="link" to='/contact'>Contact</Link>
      </div>
      <Switch location={isModal ? this.previousLocation : location}>
        <Route exact path="/" component={Home} />
        <Route exact path="/contact/" component={Contact} />
        <Route exact path="/about" component={About} />
        <Route exact path="/modal/:id" component={Modal} />
        <Route>{'no match'}</Route>
      </Switch>
      {isModal
        ? <Route exact path="/modal/:id" component={Modal} />
        : null
      }
    </div>
  );
}
```

### Rendering Different Modal Views

So far we have implemented our modal in a way that ensures we don’t render an overlay when refreshing a page with an open modal, or when directly visiting a modal route.

Instead, we only render the matching `<Route/>` inside `<Switch/>`.

In this case, the styling you want to apply is likely to be different, or you might want to show a different content.

This is pretty easy to achieve by passing the `isModal` variable as a prop on the `<Modal/>` component, as shown below.

Then, depending on the value of the prop, you can apply different stylings or return a completely different markup.

```jsx
return (
  <div className="app">
    <div className="menu">
      <Link className="link" to='/'>Home</Link>
      <Link className="link" to='/about'>About</Link>
      <Link className="link" to='/contact'>Contact</Link>
    </div>
    <Switch location={isModal ? this.previousLocation : location}>
      <Route exact path="/" component={Home} />
      <Route exact path="/contact/" component={Contact} />
      <Route exact path="/about" component={About} />
      <Route exact path="/modal/:id" component={Modal} />
      <Route>{'no match'}</Route>
    </Switch>
    {isModal
      ? <Route exact path="/modal/:id">
          <Modal isModal />
        </Route>
      : null
    }
  </div>
);
```

### Preventing the Scroll Underneath the Modal

When you open the modal on some browsers it may have the content below scrolling underneath the modal, which is not a desirable interaction.

Using `overflow: hidden` on `body` is the first attempt to block scrolling on the entire page.

However, although this method works fine on desktop, it fails on mobile Safari since it basically ignores `overflow: hidden` on `body`.

There are several different npm packages attempting to remedy this scroll locking issue virtually across all platforms.

I found the [body-scroll-lock](https://www.npmjs.com/package/body-scroll-lock) package quite useful.

From this package, you can import `disableBodyScroll` and `enableBodyScroll` functions, which accept a reference to the element for which you want scrolling to persist as an input.

When the modal is open we want to disable scrolling for the entire page, except for the modal itself.

Therefore, we need to call `disableBodyScroll` and `enableBodyScroll` functions when the modal component is mounted and unmounted, respectively.

To get a reference to the parent `<div>` of the modal component, we can use the `createRef` API from React and pass it as a ref to the parent `<div>`.

The code snippet below disables scrolling when the modal is open and enables it again when the modal component is about to be unmounted.

Using `this.modalRef` as the input for these imported functions prevents the content of the modal component from being scroll-locked.

Before using the `disableBodyScroll` function, we need a simple check.

This is because a modal component might get mounted if the page is refreshed when a modal is open, or when the modal route is visited directly.

In both cases, scrolling should not be disabled.

We have already passed the `isModal` variable as a prop to the `<Modal/>` component to render different views, so we can just use this prop to check if there is actually a modal.

Below is the modified version of the modal component:

```jsx
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';

class Modal extends Component {
  constructor(props) {
    super(props);
    this.modalRef = React.createRef();
  }
  
  componentDidMount() {
    const { isModal } = this.props;

    if (isModal) {
      disableBodyScroll(this.modalRef.current);
    }
  }
  
  componentWillUnmount() {
    enableBodyScroll(this.modalRef.current);
  }
  
  render() {
    return (
      <div
        ref={this.modalRef}
        className="modal-wrapper"
        onClick={() => this.props.history.goBack()}
      >
        <div
          className="modal"
          onClick={e => e.stopPropagation()}
        >
        </div>
      </div>
    )
  }
```

### Conclusion

You now have an understanding of how a modal view works, as well as a sense of some of the problems you may encounter while implementing your own integration.

For the fully functional example, visit [this](https://codesandbox.io/s/jolly-jones-cx4cw) code sandbox project.
* * * 
**Editor's note:** Seeing something wrong with this post? You can find the correct version [here](https://blog.logrocket.com/building-a-modal-module-for-react-with-react-router/).

## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *
The post [Building a modal module for React with React-Router](https://blog.logrocket.com/building-a-modal-module-for-react-with-react-router/) appeared first on [LogRocket Blog](https://blog.logrocket.com).