React Modals — scalable, customizable, neat components!

Why should you care?

If you haven’t tried to implement modals in React, you wouldn’t understand the context of this blog. If you had, there is a chance you have wandered into some random novice JS developer’s blog (I’m talking about this one) because, quite frankly… there isn’t a whole lot of guidance re: modals in React. Of course, this isn’t to say that there aren’t ANY good resources on the subject out there… I certainly wouldn’t have gotten to where I am now without them. However, I found their implementations confusing (for a beginner like myself), conflicting, and/or vague.

BUT! Here is a collection of a few gems that I have found — where the rest of my blog is based heavily upon:

I recommend checking out those links — but you don’t have to.

It is mainly to prove a point… modals in React are a pain to implement… even if you use a library like react-modal (sometimes there is too much abstraction).

For the purposes of this blog post, I will only be speaking about modals in React. I wouldn’t be surprised if it were easier to do in Angular, or some other framework.

In short, modals are simply “hacky” HTML/CSS views combined with some event-handlers.

I write “hacky” in quotes because messing around with HTML and CSS seems like something we shouldn’t do much of in React. But with modals, it’s unavoidable. If you hate CSS, turn around and forget about modals altogether before it’s too late. But remember, you will never have a cool login pop-up suited to your design tastes. :)

Back on topic: Modals are a rendering of some HTML elements with certain CSS classes triggered by some event-handler on a button, link, or whatever. Basically, you click a button, the pop-up is rendered on your screen! The magic is that the website you were just on does not go away! You can still see the page behind the pop-up!

There are three major HTML elements rendered when the modal pops up:

  1. An Overlay — typically a <div> element with a class that creates a “shadow” effect over the document while the modal is onscreen.
  2. A Content Box — another <div> element with a class that provides the position context for the dialog/modal box (#3).
  3. A Dialog/Modal Box — yet another <div> element with a class that resizes itself to the content of the modal.
That’s it! That’s the skeleton of a basic modal. However, the real challenge is how to make a re-usable modal with customizable design inside your React application.

Why Something Seemingly Simple Becomes Hard With React

Important: The rest of this post will assume you have a solid grasp of how components are often structured in react with redux (and react-redux).

If you are anything like me, checking out the links above might have led to more confusion.

Here is what I want my modal to do (and what you probably want as well!):

  1. Reusable — I don’t want to create the HTML/CSS for every modal I put into my app. Who does?
  2. Scalable — I want my modals to be rendered from a centralized location for organization purposes.
  3. Redux-ified — I want to connect my modal to my redux store because I may want to pass props down.
  4. Customizable — I don’t want all my modals to look the same.

We will take these one-by-one below. Whee!


1. Reusable — make a default Modal component!

You could import in a nice package like react-modal to do this for you, but you won’t have as much control over its customization and/or its connection to the redux store. But it’s up to you!

You will need to create a class for your default Modal component.

return (
<div>
<div className="modal-overlay-div" style={overlayStyle} />
<div className="modal-content-div" style={contentStyle} onClick={this.onOverlayClick.bind(this)}>
<div className="modal-dialog-div" style={dialogStyle} onClick={this.onDialogClick}>
{this.props.children}
</div>
</div>
</div>
);

The rendered portion of the default Modal component is what I mentioned earlier, namely the three HTML elements (Overlay, Content Box, Dialog Box) and their CSS classes that render the Modal.

  • You will need to give default CSS classes to the elements. The corresponding CSS is more boilerplate as well (presumably inserted into your custom css file):
.modal-overlay-div {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 1000;
background-color: rgba(0, 0, 0, .65);
}

.modal-content-div {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10000;
overflow: auto;
text-align: center;
padding: 4px;
cursor: pointer;
}

.modal-content-div:after {
vertical-align: middle;
display: inline-block;
height: 100%;
margin-left: -.05em;
content: '';
}

.modal-dialog-div {
position: relative;
outline: 0;
width: auto;
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
max-width: auto;
cursor: default;
border-radius: 4px;
}

The Overlay class has a high z-index that places itself over the current page. It covers the entirety of the page and its color is set to black with the opacity set to .65. Obviously, you could change that if you wish.

The Content Box class has an even higher z-index placed on top of the overlay. It also covers the page and it provides positioning context for its child Dialog Box.

The Dialog Box class, as a child to the Content Box, has its position relative to the Content Box. Anything that goes inside the Dialog Box (your actual modal content), will resize the Dialog Box accordingly.

  • Last but not least, the Modal methods! (more boilerplate)
listenKeyboard(event) {
if (event.key === 'Escape' || event.keyCode === 27) {
this.props.onClose();
}
}

componentDidMount() {
if (this.props.onClose) {
window.addEventListener('keydown', this.listenKeyboard.bind(this), true);
}
}

componentWillUnmount() {
if (this.props.onClose) {
window.removeEventListener('keydown', this.listenKeyboard.bind(this), true);
}
}

onOverlayClick() {
this.props.onClose();
}

onDialogClick(event) {
event.stopPropagation();
}

These methods allow user input to create expected default actions.

  • listenKeyboard “listens” to the user keyboard to see if they have pressed “ESC” while the modal is open. If so, the modal closes.
  • onOverlayClick means that if you click on the overlay (outside the Modal Dialog Box), it will close the modal (an expected outcome).
  • onDialogClick simply prevents the closing of the modal when you click within the Dialog Box (because it will close when you click the Overlay that covers the whole page at a lower z-index). If this confuses you, try disabling it and testing the modal — you will see why this is necessary!

Very Important! If you note, these methods take in an onClose method to cause the Modal to close! In other words, if an onClose method is NOT passed into the Modal component as a prop, your modal will not close as expected! But we see how that’s done below :)

These methods are passed through the respective <div>s to give you the… Full Modal Experience. We still haven’t touched on the customized styles (which we will touch upon last).

Now, you have created your default Modal component! You can now import this modal anywhere in your App for use!


2. Scalable — create a ModalRoot container

If your website anticipates more than a single kind of modal (e.g., Login Modal, Warning Modal, Registration Modal, Terms of Services Modal, etc.), it may be wise to separate the presentational modal component from a container that handles which presentational component ought to be rendered.

The following code will do just that!

import React from 'react';
import { connect } from 'react-redux';

/** Modal Components */
import LoginModal from './components/LoginModal';
import SignupModal from './components/SignupModal';

/** Modal Type Constants */
import { LOGIN_MODAL, SIGNUP_MODAL } from './modaltypes';

const MODAL_COMPONENTS = {
LOGIN_MODAL: LoginModal,
SIGNUP_MODAL: SignupModal
};

const ModalContainer = (props) => {
if (!props.modalType) {
return null;
}

const SpecificModal = MODAL_COMPONENTS[props.modalType];

return <SpecificModal />;
};

const mapStateToProps = state => {
return {
modalType: state.modal.modalType
};
};

export default connect(mapStateToProps)(ModalContainer);

At first glance, this may be a bit confusing, but we will take it step by step. It is also difficult to read and understand this snippet without the proper context (e.g., what is being passed in, where are all these things located).

Things to note:

  1. We are importing all the unique modal components into this container! (e.g., LoginModal, WarningModal, etc.)
  2. We have constants imported in for all the different modal types because this Modal Container is connected to the store and will be receiving a prop as props.modalType (via mapStateToProps) to figure out which modal to render.
  3. The MODAL_COMPONENTS object maps each modal component to their respective modalType (from the redux store).
  4. Lastly, the ModalContainer will receive props and checks if the store has a specific modalType in mind to be rendered; if no modalType is set in the store, nothing is rendered from the container. However, if there IS a modalType set on the store, then the ModalContainer will check the MODAL_COMPONENTS object, grab the corresponding modal and render that out.

This way, you have a predictable location (container) from which all your different modals will be rendered!

It is understandable to be confused at this point, because we have not covered the redux portion yet, but since that’s up next, we will perhaps get a better sense of how this all comes together!


3. Redux-ified — connect it to the store as a child of the App component

First things first, we need to somehow connect this ModalContainer to someplace so that it will be rendered on our HTML! Right now, the ModalContainer and its soon-to-be-rendered Modal Components are out in limbo. Where should we anchor it?

There is a whole debate on where best to do this. But I’ll cut to the chase — it is best here.

import React from 'react';
import NavBarContainer from '../containers/NavBarContainer';
import ModalContainer from '../modals/ModalContainer';

export default function App ({ children }) {
return (
<div id="main">
<NavBarContainer />
{children}
<ModalContainer />
</div>
);
}

Why? Because it avoids the messyness of having to pull in another Providerfrom react-redux and ReactDom.render to connect to the store and attaching a new element directly to the index.html file. Don’t understand that? It really doesn’t matter because you don’t need to!

Anyways, if you look at the image above, the App component is the parent to the ModalContainer. The App component is the parent component to all other components, as seen below:

export function Root (props) {
return (
<Router history={browserHistory}>
<Route path="/" component={App} onEnter={props.onAppEnter}>
<Route .... nested routes here...
</Route>
</Router>
);
}

I am using react-router here.

Next, all the routes are connected to the redux store via the Provider (react-redux) as follows:

ReactDOM.render(
<Provider store={store}>
<Root />
</Provider>,
document.getElementById('app')
);

Lastly, we have to create some actions and a reducer to handle those actions in the store!

There are only two must-have actions you have to set up for your modals:

/** Constants */
export const SHOW_MODAL = 'SHOW_MODAL';
export const HIDE_MODAL = 'HIDE_MODAL';

/** Action-creators */
export const loadModal = (modalType) => {
return {
type: SHOW_MODAL,
modalType
};
};

export const hideModal = () => {
return {
type: HIDE_MODAL
};
};
  1. SHOW_MODAL — takes in a modalType to set the modalType in the store.
  2. HIDE_MODAL — simply an action that will set the modalType to null in the store.
/** Constants */
import { SHOW_MODAL, HIDE_MODAL } from '../action-creators/modal';

/** Initial State */
const initialModalState = {
modalType: null
};

/** Modal reducer */
export default function (state = initialModalState, action) {
const newState = Object.assign({}, state);

switch (action.type) {

case SHOW_MODAL:
newState.modalType = action.modalType;
break;

case HIDE_MODAL:
return initialModalState;

default:
return state;
}

return newState;
}

Here we see the reducer for the modals.

Initially, the store’s modalType is set to null. However, once an showModal action is dispatched from wherever in your app (with the desired modalType), it will set the store’s modalType to the desired modal. This will then trigger the mapStateToProps in our ModalContainer (connected to the redux store via the Provider as a child of the App component) and now we have props.modalType set to the desired modal component. As a result, ModalContainer will render that desired Modal component!

The hideModal is also dispatched from your app and will set the store’s modalType back to null (hiding the modal again).

Almost done!


Bringing it all together… almost!

Bringing this together involves two things (1) creating your presentational modal component and (2) triggering the showModal action somewhere in our app (think: a Login button)!

1. Basic Login Modal Component

import { hideModal } from '../../action-creators/modal';
import Modal from '../Modal';

class LoginModal extends React.Component {
constructor(props) {
super(props);
this.onClose = this.onClose.bind(this);
}

onClose() {
this.props.hideModal();
}

render() {
return (
<Modal onClose={this.onClose}>
<div className="login">
<h1>Login</h1>
<form ...login form...
</div>
</Modal>
);
}
}

There a few important things to note here:

  • First, we are importing in the default Modal component we created in Step 1 of this blog.
  • Second, we have an onClose method defined here. Remember earlier I said this has to be passed in as a prop to the default Modal component for it to trigger the different methods to exit out of the modal (e.g., onOverlayClick). It is inside this onClose method that we will pass in our hideModal action-creator to set the store’s modalType to null.

2. Login Button!

If you have separated your presentational components from your container components react-redux style, then what follows below will make sense. If you are unfamiliar with the presentation-container divide, the presentational component is merely receiving props from the container and handles all rendering.

  • NavBar Presentational Component
<ul className="nav navbar-nav navbar-right">
<li><a onClick={this.props.showLoginMenu}>LOGIN</a></li>
</ul>

Here we see a very basic navigation bar for our app. It only contains a single LOGIN button. But as you can see, the button has a showLoginMenu method (passed down from the container) that will be triggered once the button is clicked.

  • NavBar Container Component
/** Action-creators */
import { loadModal } from '../action-creators/modal';

/** Modal Type Constant */
import { LOGIN_MODAL } from '../modals/modaltypes';

export class NavBarContainer extends React.Component {
constructor(props) {
super(props);
this.showLoginMenu = this.showLoginMenu.bind(this);
}

showLoginMenu() {
this.props.loadModal(LOGIN_MODAL);
}

render() {
return (
<NavBar
showLoginMenu={this.showLoginMenu}
/>
);
}
}

const mapDispatchToProps = dispatch => ({
loadModal: modelType => dispatch(loadModal(modelType))
});

export default connect(null, mapDispatchToProps)(NavBarContainer);

Here, we see how the presentational component receives the showLoginMenu method. We imported the loadModal action-creator and will invoke it within the showLoginMenu method inside the NavBar Container with the LOGIN_MODAL modalType!

This is very important. We are saying that once the button is clicked in our presentational component, it will set the store’s modalType to LOGIN_MODALand will trigger the ModalContainer to render the LoginModal via const SpecificModal = MODAL_COMPONENTS[props.modalType]!

Now, if you click on that Login button, you will get your Login Modal popping up on your screen! If you click outside the modal or press ‘ESC’ it exits the modal too! Cool!

But what if you want to make some changes to the default Modal? What if you want it positioned differently, or with a different background?


4. Customizable — pass in custom styles to the default Modal component to overwrite the default CSS

This is a simple solution but is my way of handling customization for an otherwise boring default Modal.

It harnesses React’s principles of allowing you to pass down almost anything as a prop to the component and the style property inside a component that can overwrite any default CSS.

All we have to do is add a few lines to our default Modal component:

render() {
const overlayStyle = this.props.overlayStyle ? this.props.overlayStyle : {};
const contentStyle = this.props.contentStyle ? this.props.contentStyle : {};
const dialogStyle = this.props.dialogStyle ? this.props.dialogStyle : {};
return (
<div>
<div className="modal-overlay-div" style={overlayStyle} />
<div className="modal-content-div" style={contentStyle} onClick={this.onOverlayClick.bind(this)}>
<div className="modal-dialog-div" style={dialogStyle} onClick={this.onDialogClick}>
{this.props.children}
</div>
</div>
</div>
);
}

Those three lines check if an overlayStyle, contentStyle, dialogStyle prop has been sent to the default Modal component. If so, it will overwrite any conflicting style in the corresponding div element.

For example:

render() {
const dialogStyle = {
backgroundColor: 'black'
};
return (
<Modal onClose={this.onClose} dialogStyle={dialogStyle}>
<div className="login">
<h1>Login</h1>
<form ...login form...
</div>
</Modal>
);
}

Now we can first SET dialogStyle in our LoginModal component; here, setting our default Modal Component’s Dialog Box element’s backgroundColor: black.

Then, we can pass dialogStyle down as a prop to the default Modal Component! The Modal Component will receive the prop, and overwrite the default backgroundColor of our Dialog Box!

Hence, we get customization without losing reusability! You don’t need to create a whole new Modal Component with its default methods like onOverlayClick. Instead, only deal with the CSS.


In Conclusion

Modals are very fun to use and makes websites feel seamless. However, as we have explored above, the implementation isn’t quite so seamless. However, this is primarily because we were concerned with making our modals in our app scalable and customizable using redux! There is so little guidance on this specific topic that I felt the need to air out my findings and my solutions to a rather complex problem.

I must give credit to the links I referenced above — particularly Mike Vercoelen’s post: Scalable Modals with React and Redux. Much of my code is copy-and-pasted from his (apart from how we handled the CSS). Hopefully my detailed overview was as helpful as it has been verbose.

Good luck!

Like what you read? Give Dan Park a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.