Scalable Modals with React and Redux

I have tried many ways for setting up a scalable way to create modals with React and Redux. The approach that I ended up using is basically a tweaked version from a Stackoverflow answer from Dan Abramov’s 

Modals

Creating modals in React is pretty simple, many solutions implement React Portals but I think rendering a Modal outside of the application container is overkill, we use a ModalRoot container which we render in our application container instead.

So I talk here about a solution that is scalable, testable and simple. It requires some boilerplate code to setup but it’s worth it in the long run.

Repository

Here is a repository with the solution, keep reading if you want a detailed explanation.

Github repository

Creating a view Modal component

First we setup a very basic Modal component, which we are going to use to implement different kinds of Modals such as Notification, Confirmation etc.

To keep things as simple as possible, I don’t use any css files, but I use styled-components. It’s a very nice library to add styling to your components, and I highly recommend you check it out, but feel free to translate the components styling to your project (scss, less etc.)

components/modal.js

import React, { Component, PropTypes } from 'react';
import styled from 'styled-components';

const Overlay = styled.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);
`;

const Content = styled.div`
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 10000;
  overflow: auto;
  text-align: center;
  overflow-scrolling: touch;
  padding: 4px;
  cursor: pointer;

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

const Dialog = styled.div`
  position: relative;
  outline: 0;
  width: 100%;
  background: white;
  display: inline-block;
  vertical-align: middle;
  box-sizing: border-box;
  max-width: 520px;
  cursor: default;
`;

const Header = styled.div`
  padding: 16px 8px 8px 8px
`;

const Body = styled.div`
  padding-bottom: 16px
`;

export default class Modal extends Component {
  static propTypes = {
    children: PropTypes.node,
    title: PropTypes.string,
    onClose: PropTypes.func
  };

  listenKeyboard = (event) => {
    if (event.key === 'Escape' || event.keyCode === 27) {
      this.props.onClose();
    }
  };

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

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

  get title () {
    const { title } = this.props;

    return title ? (
      <div className='modal__title'>
        <h1>{title}</h1>
      </div>
    ) : null;
  }

  get close () {
    const { onClose } = this.props;

    return onClose ? (
      <div className='modal__close' onClick={onClose} />
    ) : null;
  }

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

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

  render () {
    return (
      <div className='modal'>
        <Overlay />
        <Content onClick={this.onOverlayClick}>
          <Dialog onClick={this.onDialogClick}>
            <Header>
              {this.title}
              {this.close}
            </Header>
            <Body>
              {this.props.children}
            </Body>
          </Dialog>
        </Content>
      </div>
    );
  }
}

Setting up the Redux magic

To create all the Redux logic and set this up correctly we are going to add/change:

  • actions/modal.js (new)
  • reducers/modal.js (new)
  • constants/ModalTypes.js (new)
  • constants/ActionTypes.js (changed)

First we have to add 2 new action types SHOW_MODAL and HIDE_MODAL to your project.

constants/actiontypes.js

// ... 

export const SHOW_MODAL = 'SHOW_MODAL';
export const HIDE_MODAL = 'HIDE_MODAL';

// ...

Next we are going to add 2 new action dispatchers showModal and hideModal

actions/modal.js

import { SHOW_MODAL, HIDE_MODAL } from '../constants/ActionTypes';

export const showModal = (type, props) => ({
  type: SHOW_MODAL,
  payload: {
    type,
    props
  }
});

export const hideModal = () => ({
  type: HIDE_MODAL
});

Now lets create a reducer to handle our actions.

NOTE: Don’t forget to include the reducer to your root reducer!

reducers/modal.js

import { SHOW_MODAL, HIDE_MODAL } from '../constants/ActionTypes';

const initialState = {
  type: null,
  props: {}
};

function modalReducer (state = initialState, action) {
  switch (action.type) {
    case SHOW_MODAL:
      return {
        ...state,
        type: action.payload.type,
        props: action.payload.props
      };
    case HIDE_MODAL:
      return initialState;
    default:
      return state;
  }
}

export default modalReducer;

Next is creating a new file in your constants ModalTypes, more info on this later.

constants/modaltypes.js

export const MODAL_TYPE_NOTIFICATION = 'MODAL_TYPE_NOTIFICATION';
export const MODAL_TYPE_CONFIRMATION = 'MODAL_TYPE_CONFIRMATION';

Creating a Notification modal type

As you can see above, in the ModalTypes file we added Notification and Confirmation as modal types, lets first create a Notification modal.

The notification modal type is just to show a popup message with a button to close it (ok).

containers/modals/notification.js

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

import { hideModal } from '../../actions/modal';
import Modal from '../../components/Modal';

const Notification = ({ title, afterClose, hideModal }) => {
  const onClose = () => {
    hideModal();

    if (afterClose) {
      afterClose();
    }
  };

  return (
    <Modal title={title} onClose={onClose}>
      <button onClick={onClose}>
        Ok
      </button>
    </Modal>
  );
};

Notification.propTypes = {
  title: PropTypes.string,
  onClose: PropTypes.func
};

export default connect(null, { hideModal })(Notification);

Creating a Confirmation modal type

Another modal type we are going to add is a confirmation, which is slightly different than our notification modal type, because it asks the user to confirm (yes / no).

confirmation.js

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

import { hideModal } from '../../actions/modal';
import Modal from '../../components/Modal';

const Confirmation = ({ title, onConfirm, hideModal }) => {
  const handleConfirm = (isConfirmed) => () => {
    hideModal();
    onConfirm(isConfirmed);
  };

  return (
    <Modal title={title}>
      <button onClick={handleConfirm(true)}>
        Yes
      </button>
      <button onClick={handleConfirm(false)}>
        No
      </button>
    </Modal>
  );
};

export default connect(null, { hideModal })(Confirmation);

Creating a ModalRoot container

The ModalRoot container which we are going to create is responsible for rendering the correct modal component in your RootContainer with its correct properties.

containers/modalroot.js

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

import Notification from './modals/Notification';
import Confirmation from './modals/Confirmation';

import { MODAL_TYPE_NOTIFICATION, MODAL_TYPE_CONFIRMATION } from '../constants/ModalTypes';

const MODAL_COMPONENTS = {
  [MODAL_TYPE_NOTIFICATION]: Notification,
  [MODAL_TYPE_CONFIRMATION]: Confirmation
};

const ModalRoot = ({ type, props }) => {
  if (!type) {
    return null;
  }

  const ModalComponent = MODAL_COMPONENTS[type];
  return <ModalComponent {...props} />;
};

export default connect(state => state.modal)(ModalRoot);

Adding the ModalRoot container to your RootContainer

The final step is to add your ModalRoot container to your RootContainer, depending on your project, this is mostly called App or Root.

So add the ModalRoot to your App container, in our example repository it looks like this:

containers/app.js

import React, { Component } from 'react';
import ModalRoot from './ModalRoot';

export default class Application extends Component {
  render () {
    return (
      <div>
        <div>
          {this.props.children}
        </div>
        <ModalRoot />
      </div>
    )
  }
}

How to use it?

So now everything is setup, but how do you use it? It’s pretty simple. Lets create an demo LoginView where we show 2 buttons:

  • Show modal: Clicking this button shows a notification modal
  • Show confirm: Clicking this button shows a confirmation modal, and when confirmed it shows a notification modal with the result (whether the user confirmed or not)

What we have to do is pretty simple:

  • We connect our View and inject the showModal action
  • We include the Modal type constant from which modals we want to show, in this case we want to show a notification and confirmation modal, so we include: MODAL_TYPE_CONFIRMATION and MODAL_TYPE_NOTIFICATION
  • Finally when we want to show a modal somewhere in our code (maybe when the user tries to login, but the username / password  was incorrect, so there?) we simply use this.props.showModal(<modal type>, <props>)

Lets look at the example situation (which I also used in the example repository)

containers/loginview.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { showModal } from '../actions/modal';
import { MODAL_TYPE_NOTIFICATION, MODAL_TYPE_CONFIRMATION } from '../constants/ModalTypes';

@connect(null, { showModal })
export default class LoginView extends Component {
  showNotification = () => {
    this.props.showModal(MODAL_TYPE_NOTIFICATION, {
      title: 'This is an awesome notification.'
    });
  };

  showConfirm = () => {
    this.props.showModal(MODAL_TYPE_CONFIRMATION, {
      title: 'Do you confirm?',
      onConfirm: (isConfirmed) => {
        this.props.showModal(MODAL_TYPE_NOTIFICATION, {
          title: 'The user did confirm: ' + isConfirmed
        });
      }
    });
  };

  render () {
    return (
      <div>
        <button onClick={this.showNotification}>
          Show modal
        </button>
        <button onClick={this.showConfirm}>
          Show confirm
        </button>
      </div>
    )
  }
}

Final thoughts

So now you have a working and scalable implementation on how to use modals in your React, Redux application.

Another great reason why this solution is so great: you can now easily show a modal with different props everywhere, so if you want to show your designer a modal you have been working on: you simply call the action with its modal type + props and just like that it’s there.

Cheers

 

  • Dan Park

    I appreciate this post as there are so few in-depth code examples that reconcile react, react-redux, and redux with modals.

    I just have one design question:
    — Could you use your Modal component CSS template (with the preset styled divs) with different modal type designs? For example, it seems that your Modal templates assumes all modal types would appear in the center of the screen — however, what if you wanted to bring in a modal type that pops up from the bottom corner of the screen? I’ve been tinkering with the code and can’t seem to figure out a solution without touching the default Modal component code.

    Thanks in advance for your answer!

    • Mike Vercoelen

      Hi Dan Park, you just wrote the first reply on my blog, awesome, thanks for making the effort and I’m glad you liked the in-depth code examples.

      About your question: Yes, of course it’s possible, and it’s actually quite simple.

      Lets say we want to create a Modal that pop’s up from the bottom left corner. What I would do is modify the Modal component with a boolean property which triggers the special styling.

      Lets call this prop “isBottomLeft”, now on the Modal component where you are using the Modal you just add …

      Now the only thing you have to do is add the logic in the Modal component which actually sets the different styling when the property was submitted to the component.

      Ofcourse you don’t have to use styled-components, like I did, so feel free to use SCSS and add the classes based on the property etc.

      If you need more help, feel free to mail me at: mike.vercoelen@gmail.com. We could even do a Google Hangout sessions if you need even more help 😉

      Thanks again for the reply and I hope I made it more clear,

      Mike

      • Dan Park

        Ah, thats a very nice way of handling it! Thanks for your reply. Once again, wonderful blog!

        • Dan Park

          Hey Mike, just another question.

          What is the purpose of the Content Div?
          As I understand, it sits on top of the Overlay Div, providing positioning context for the Dialog Div. However, if you made the Dialog Div a direct child of the Overlay Div, wouldn’t it accomplish the same result?

          Thanks!

          • Mike Vercoelen

            Hi Dan, good question.

            The way it is setup, is I used a trick to horizontally / vertically center the div, which requires a wrapper div (which is in this case the Content div). You can read more about it here: https://css-tricks.com/centering-in-the-unknown/

            So there are different way in how you can set up the styling of the Modal component, it is totally up to you 😉

            Feel free to play around, and if you have more questions, ask ask ask.

            Mike

  • Diana

    Hi Mike,

    Thanks for this great writeup!

    I have a couple silly questions.

    The ModalRoot container expects an object with type and props, but it is rendered in the Application class with no arguments. How does the container get those values?

    Also in ModalRoot the connect call is made with a mapStateToProps of just ‘state => state.modal’. My react/redux knowledge is a bit lacking (probably js too), so I don’t understand what this does.