How to make routable modals in react with react-router
Tags: tutorial, javascript, react
Published: 2020-11-26 17:17:28

### Introduction

Modals are an integral part of web apps. They come in handy when you want to display content that isn't necessarily large enough to take up its own page or content that depends on the current view but ideally should be standalone to give it more emphasis. Because modals are essentially an overlay on a parent page, it makes them tricky to add to our routing system. In this article your're to learn how to add route navigation to your modals.

### Prerequisite
> Basic knowledge of react, react hooks, and react-router.
React >=16.8

We're going to start by creating a simple react application which displays a list of contacts. You can set up your application locally with create-react-app or for convenience, use an online playground like [codesanbox](https://codesandbox.io) or [stackblitz](https://stackblitz.com). I'm using stackblitz and there'll be a link to the playground at the end of the post.

Our react app has 4 components (Home, Contacts, Card and Modal). The `Home` component just renders a welcome text and a link to the contacts page. The `Contacts` component renders a list of cards and the `Card` component in turn contains a link to the modal. I'll be using tailwindcss for styling, again for convenience since the focus of this article is on react-router. Let's create a router component and add the home and contacts page.
App.js
```js
import React from "react";
import {
  Switch,
  Route,
  useLocation
} from "react-router-dom";
import Home from "./Home";
import Contacts from "./Contacts";

export default function App() {
  return (
    <div className="w-full bg-gray-200 px-4 relative">
      <Switch>
        <Route path="/" exact component={Home} />
        <Route path="/contacts" exact component={Contacts} />
      </Switch>
    </div>
  );
}
```
index.js
```js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";

import App from "./App";

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById("root")
);
```
> We put our `Router` in the `index.js` file instead of `App.js` because we would later need to access some hooks in `App.js` which will cause errors if we did otherwise.

Home.js
```js
import React from "react";
import { Link } from "react-router-dom";

const Home = () => {
  return (
    <div className="w-full h-screen flex flex-col justify-center items-center">
      <h1 className="text-center text-3xl text-gray-600 font-medium">Welcome!</h1>
      <Link to="/contacts">
        <button className="rounded-lg bg-indigo-400 px-4 py-2 mt-4 text-white font-bold hover:bg-indigo-500">Contacts</button>
      </Link>
    </div>
  )
}

export default Home;
```
> The `Home` component is very simple. We use a Link component from react-router to navigate to the contacts page.

Contacts.jsx
```js
import React from "react";
import Card from "./Card";

const Contacts = () => {
  return (
    <div className="pt-16 w-full">
      <h1 className="text-2xl font-semibold text-gray-600 text-center">Contacts</h1>
      <div className="flex justify-center flex-wrap mt-8">
        {Array(6).fill().map(() => <Card />)}
      </div>
    </div>
  );
};

export default Contacts;
```
> In the contacts page, we render a list of cards. Since we don't have any actual data we just create an arbitrary array of size 6 and fill it with `undefined` so we can map to `Card` components.

Card.jsx
```js
import React from "react";
import { Link } from "react-router-dom";

const Card = () => {
  return (
    <div className="w-56 pb-2 mt-8 mx-4 bg-white rounded-md border border-gray-200 overflow-hidden shadow-lg">
      <Link
        to="/contact/andrew-garfield">
        <div className="flex flex-col items-center py-4 px-2 bg-gray-300">
          <span className="w-10 h-10 rounded-full overflow-hidden inline-block">
            <img
              src="https://uifaces.co/our-content/donated/gPZwCbdS.jpg"
              alt=""
            />
          </span>
          <h1 className="text-lg font-medium text-gray-600 mt-2">
            Andrew Garfield
          </h1>
          <p className="text-sm text-gray-600">Project Manager</p>
        </div>
      </Link>
      <div className="px-2 py-2">
        <p className="text-sm text-gray-600 mt-1 flex items-center">
          <svg
            className="w-4 h-4 mr-2"
            fill="currentColor"
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              fill-rule="evenodd"
              d="M6 6V5a3 3 0 013-3h2a3 3 0 013 3v1h2a2 2 0 012 2v3.57A22.952 22.952 0 0110 13a22.95 22.95 0 01-8-1.43V8a2 2 0 012-2h2zm2-1a1 1 0 011-1h2a1 1 0 011 1v1H8V5zm1 5a1 1 0 011-1h.01a1 1 0 110 2H10a1 1 0 01-1-1z"
              clipRule="evenodd"
            />
            <path d="M2 13.692V16a2 2 0 002 2h12a2 2 0 002-2v-2.308A24.974 24.974 0 0110 15c-2.796 0-5.487-.46-8-1.308z" />
          </svg>
          Voyance
        </p>
        <p className="text-sm text-gray-600 mt-1 flex items-center">
          <svg
            className="w-4 h-4 mr-2"
            fill="currentColor"
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
            <path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
          </svg>
          andrew@hey.com
        </p>
        <p className="text-sm text-gray-600 mt-1 flex items-center">
          <svg
            className="w-4 h-4 mr-2"
            fill="currentColor"
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              fill-rule="evenodd"
              d="M7 2a2 2 0 00-2 2v12a2 2 0 002 2h6a2 2 0 002-2V4a2 2 0 00-2-2H7zm3 14a1 1 0 100-2 1 1 0 000 2z"
              clipRule="evenodd"
            />
          </svg>
          +440-344-45-577
        </p>
        <p className="text-sm text-gray-600 mt-1 flex items-center">
          <svg
            className="w-4 h-4 mr-2"
            fill="currentColor"
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
            <path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
          </svg>
          @andrew_garfield
        </p>
      </div>
    </div>
  );
};

export default Card;
```
> The `Card` component contains a `Link` that **navigates** to the modal which just displays the a single card but the content is not the focus. Let's now create the logic to make a routable modal.

For our modal to be routable we need to render it in a route component but also make sure that we don't leave the current page when navigating to the modal. We want it to behave like a sub-route. Let's update our `App.js`
App.js
```js
import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  useLocation
} from "react-router-dom";
import Home from "./Home";
import Contacts from "./Contacts";
import Modal from "./Modal";
import "./style.css";

export default function App() {
  const location = useLocation();
  const background = location.state && location.state.background;

  return (
    <div className="w-full bg-gray-200 px-4 relative">
      <Switch location={background || location}>
        <Route path="/" exact component={Home} />
        <Route path="/contacts" exact component={Contacts} />
      </Switch>

      {background && <Route path="/contact/:name" children={<Modal />} />}
    </div>
  );
}
```
> We've introduced two new things; `location` and `background`. `location` is an object that contains `url` information which we get from the `useLocation` hook. It updates to the new `url` whenever we navigate to a new page. `background` however represents the location state that we were right before we navigate to the modal. Remember we don't actually want to leave the current page, that's why we don't render the `Route` component for the modal in the `Switch` component, rather we put it outside and conditionally render depending on the value of `background`.

What this means is that if there's a background state (it suggests that we are routing to a modal and we don't want to leave the current page), then use the background state as the location for the `Switch` so that we can still show the current page behind the modal. You should notice that we're passing a location prop to the `Switch` component whose value is either the `background` (if it exists) or the new `location` set by `useLocation`.

You might be wondering where exactly this `background` state comes from. Well, we set it in the `Link` component that navigates to the modal. Since we navigate to our modal from the `Card` component, let's update it to reflect that.
Card.jsx
```js
import React from "react";
import { Link, useLocation } from "react-router-dom";

const Card = () => {
  const location = useLocation();
  return (
    <div className="w-56 pb-2 mt-8 mx-4 bg-white rounded-md border border-gray-200 overflow-hidden shadow-lg">
      <Link
        to={{
          pathname: "/contact/andrew-garfield",
          state: { background: location }
        }}
      >
        <div className="flex flex-col items-center py-4 px-2 bg-gray-300">
          <span className="w-10 h-10 rounded-full overflow-hidden inline-block">
            <img
              src="https://uifaces.co/our-content/donated/gPZwCbdS.jpg"
              alt=""
            />
          </span>
          <h1 className="text-lg font-medium text-gray-600 mt-2">
            Andrew Garfield
          </h1>
          <p className="text-sm text-gray-600">Project Manager</p>
        </div>
      </Link>
...
```
> I'm showing only the part of the `Card` component that changed. We now pass an object to the `Link` component which contains two fields; `pathname` and `state`. `pathname` is the page we're navigating to and `state` is an object we can pass user defined variables to. So we set the `background` here which is just the current location we get from `useLocation`. This is how we tell react-router to use the current location instead of updating to a new location object.

I believe you get the concept now, whenever we want to navigate to a modal, we set the `background` state to tell react-router that we do not want to leave the current page but just display the modal as an overlay. This gives us the ability to treat the modal as a normal page and use features like `history.goBack`. I left the `Modal` component for last so you can see this in action.
Modal.jsx
```js
import React from "react";
import { useHistory } from "react-router-dom";
import Card from "./Card";

const Modal = () => {
  const history = useHistory();

  const closeModal = e => {
    e.stopPropagation();
    history.goBack();
  };

  React.useEffect(() => {
    document.body.classList.add("overflow-hidden");

    return () => {
      document.body.classList.remove("overflow-hidden");
    };
  }, []);

  return (
    <div className="absolute inset-0 bg-black bg-opacity-75 w-full h-screen z-10 flex items-center justify-center">
      <span
        className="inline-block absolute top-0 right-0 mr-4 mt-4 cursor-pointer"
        onClick={closeModal}
      >
        <svg
          class="w-6 h-6 text-white"
          fill="currentColor"
          viewBox="0 0 20 20"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            fill-rule="evenodd"
            d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
            clip-rule="evenodd"
          />
        </svg>
      </span>
      <Card />
    </div>
  );
};

export default Modal;
```
> Here, we get the history object from the `useHistory` hook. We can then call `history.goBack()` when we want to close the modal to navigate to the previous page, only that the previous page is in-fact the current page. 

**pageCeption**
![mind blown meme](https://dev-to-uploads.s3.amazonaws.com/i/2c1i6y4qxg8qmf4ug6lm.jpg)
Haha, I'm sorry.

Alright, that is the end of this post. Hopefully, it's not too long and you've learned something new. Here's a link to the [demo](https://stackblitz.com/edit/react-routable-modal?file=src/Modal.jsx) and a github [repo](https://github.com/Mayowa-Ojo/routable-modal)