Modals in React/Redux

Posted by andrewjford on October 25, 2017

Recently I have been working on adding login and signup modals to one of my React/Redux applications. These modals would be accessed from the navbar, but I was unsure of how they should be structured in a React and Redux design. In this post I will outline a common approach for handling modals in a React/Redux application.

The general overview of this approach is to have a single modal container which displays modals based on what you have in your Redux store. Components that trigger a modal, such as a link in the navbar, would dispatch actions to the modal store. This centralizes all the modals in the web app which I felt was a clean way to organize and structure modals.

In this example I will use a login modal activated from the navbar.

// Navbar.js
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { openLoginWindow } from '../actions/modalActions';

class Navbar extends React.Component {

  handleLoginClick = (event) => {
    this.props.openLoginWindow();
  }

  render() {
    return <nav>
        <NavLink
          to="/about"
          >About</NavLink>

        <Link
          onClick={this.handleLoginClick}
          to={`/`}
          >Login</Link>
      </nav>
  }
}

const mapDispatchToProps = (dispatch) => {
  return bindActionCreators({
    openLoginWindow: openLoginWindow,
  }, dispatch)
}

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

Here we connect our Navbar to be able to dispatch an openLoginWindow action in modalActions.

// modalActions.js
export function openLoginWindow() {
  return {
    type: 'OPEN_LOGIN_WINDOW'
  }
}

export function closeLoginWindow() {
  return {
    type: 'CLOSE_LOGIN_WINDOW'
  }
}

export function openSignupWindow() {
  return {
    type: 'OPEN_SIGNUP_WINDOW'
  }
}

export function closeSignupWindow() {
  return {
    type: 'CLOSE_SIGNUP_WINDOW'
  }
}

We create an actions file called modalActions. Here we have actions to open and close the Login modal and the Signup modal. The actions are used in the modalReducer.

// modalReducer.js
function modalReducer(
  state = {
    loginOpen: false,
    signupOpen: false,
  }, action) {

  switch(action.type){
    case "OPEN_LOGIN_WINDOW":
      return {...state, loginOpen: true}
    case "CLOSE_LOGIN_WINDOW":
      return {...state, loginOpen: false}
    case "OPEN_SIGNUP_WINDOW":
      return {...state, signupOpen: true}
    case "CLOSE_SIGNUP_WINDOW":
      return {...state, signupOpen: false}
    default:
      return state;
  }
}

export default modalReducer;

The reducer that takes in the modal actions stores a boolean for whether a modal should be open or not.

Don’t forget to add the new modalReducer to your store. Below I add the modalReducer to the rootReducer in index.js.

// index.js
// lots of imports....

const rootReducer = combineReducers({
  map: mapReducer,
  user: userReducer,
  session: sessionReducer,
  modal: modalReducer,
});

const store = createStore(
  rootReducer, composeWithDevTools(applyMiddleware(thunk),)
);

// other code....

We use a container to hold the modals. Since this is meant to be an app-wide modal container, it should be rendered in one of your main components. This container connects to state.modal in the store, and displays the modals based on those values.

// ModalContainer.js
import React from 'react';
import { connect } from 'react-redux';

import Login from '../components/Login';
import SignupModal from '../components/SignupModal';

class ModalContainer extends React.Component {
  render() {

    return <div>
      {this.props.modal.loginOpen ? <Login /> : null}
      {this.props.modal.signupOpen ? <SignupModal /> : null}
    </div>
  }
}

const mapStateToProps = (state) => {
  return {
    modal: state.modal
  }
}

export default connect(mapStateToProps)(ModalContainer);

The Signup component is set to display from the modal container when the modal store value signupOpen is true.

// Signup.js
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import SignupForm from './SignupForm';
import {
  openLoginWindow,
  closeSignupWindow,
} from '../actions/modalActions';
import { clearLoginInput } from '../actions/sessionActions';

class SignupModal extends React.Component {

  handleOutsideClick = (event) => {
    if(event.target.classList.contains("overlay-blanket")){
      this.props.closeSignupWindow();
      this.props.clearLoginInput();
    }
  }

  handleLoginLink = (event) => {
    event.preventDefault();
    this.props.openLoginWindow();
    this.props.closeSignupWindow();
  }

  render(){
    return <div className="overlay-blanket" onClick={this.handleOutsideClick}>
      <div className="center-overlay">
        <div className="center-relative">
          <h2>Signup</h2>
          <SignupForm />
          <span>
            Already a member? <a href="" onClick={this.handleLoginLink}>Login</a>
          </span>
        </div>
      </div>
    </div>
  }
}

const mapStateToProps = (state) => {
  return {
    session: state.session
  }
}

const mapDispatchToProps = (dispatch) => {
  return bindActionCreators({
    openLoginWindow: openLoginWindow,
    closeSignupWindow: closeSignupWindow,
    clearLoginInput: clearLoginInput,
  }, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(SignupModal);

The modal component itself renders from the ModalContainer. In this example I implement simple modal functionality using my own CSS and Javascript implementation.

I recommend checking out react-modal for implementing the actual modal component itself. It has accessibility features handled correctly (unlike my simple example above). Accessibility with modals can be tricky and using react-modal helps ensure proper accessibility of your modal.