[javascript] How can I display a modal dialog in Redux that performs asynchronous actions?

I'm building an app that needs to show a confirm dialog in some situations.

Let's say I want to remove something, then I'll dispatch an action like deleteSomething(id) so some reducer will catch that event and will fill the dialog reducer in order to show it.

My doubt comes when this dialog submits.

  • How can this component dispatch the proper action according to the first action dispatched?
  • Should the action creator handle this logic?
  • Can we add actions inside the reducer?

edit:

to make it clearer:

deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id)

createThingB(id) => Show dialog with Questions => createThingBRemotely(id)

So I'm trying to reuse the dialog component. Showing/hiding the dialog it's not the problem as this can be easily done in the reducer. What I'm trying to specify is how to dispatch the action from the right side according to the action that starts the flow in the left side.

This question is related to javascript modal-dialog redux react-redux

The answer is


Wrap the modal into a connected container and perform the async operation in here. This way you can reach both the dispatch to trigger actions and the onClose prop too. To reach dispatch from props, do not pass mapDispatchToProps function to connect.

class ModalContainer extends React.Component {
  handleDelete = () => {
    const { dispatch, onClose } = this.props;
    dispatch({type: 'DELETE_POST'});

    someAsyncOperation().then(() => {
      dispatch({type: 'DELETE_POST_SUCCESS'});
      onClose();
    })
  }

  render() {
    const { onClose } = this.props;
    return <Modal onClose={onClose} onSubmit={this.handleDelete} />
  }
}

export default connect(/* no map dispatch to props here! */)(ModalContainer);

The App where the modal is rendered and its visibility state is set:

class App extends React.Component {
  state = {
    isModalOpen: false
  }

  handleModalClose = () => this.setState({ isModalOpen: false });

  ...

  render(){
    return (
      ...
      <ModalContainer onClose={this.handleModalClose} />  
      ...
    )
  }

}

In my opinion the bare minimum implementation has two requirements. A state that keeps track of whether the modal is open or not, and a portal to render the modal outside of the standard react tree.

The ModalContainer component below implements those requirements along with corresponding render functions for the modal and the trigger, which is responsible for executing the callback to open the modal.

import React from 'react';
import PropTypes from 'prop-types';
import Portal from 'react-portal';

class ModalContainer extends React.Component {
  state = {
    isOpen: false,
  };

  openModal = () => {
    this.setState(() => ({ isOpen: true }));
  }

  closeModal = () => {
    this.setState(() => ({ isOpen: false }));
  }

  renderModal() {
    return (
      this.props.renderModal({
        isOpen: this.state.isOpen,
        closeModal: this.closeModal,
      })
    );
  }

  renderTrigger() {
     return (
       this.props.renderTrigger({
         openModal: this.openModal
       })
     )
  }

  render() {
    return (
      <React.Fragment>
        <Portal>
          {this.renderModal()}
        </Portal>
        {this.renderTrigger()}
      </React.Fragment>
    );
  }
}

ModalContainer.propTypes = {
  renderModal: PropTypes.func.isRequired,
  renderTrigger: PropTypes.func.isRequired,
};

export default ModalContainer;

And here's a simple use case...

import React from 'react';
import Modal from 'react-modal';
import Fade from 'components/Animations/Fade';
import ModalContainer from 'components/ModalContainer';

const SimpleModal = ({ isOpen, closeModal }) => (
  <Fade visible={isOpen}> // example use case with animation components
    <Modal>
      <Button onClick={closeModal}>
        close modal
      </Button>
    </Modal>
  </Fade>
);

const SimpleModalButton = ({ openModal }) => (
  <button onClick={openModal}>
    open modal
  </button>
);

const SimpleButtonWithModal = () => (
   <ModalContainer
     renderModal={props => <SimpleModal {...props} />}
     renderTrigger={props => <SimpleModalButton {...props} />}
   />
);

export default SimpleButtonWithModal;

I use render functions, because I want to isolate state management and boilerplate logic from the implementation of the rendered modal and trigger component. This allows the rendered components to be whatever you want them to be. In your case, I suppose the modal component could be a connected component that receives a callback function that dispatches an asynchronous action.

If you need to send dynamic props to the modal component from the trigger component, which hopefully doesn't happen too often, I recommend wrapping the ModalContainer with a container component that manages the dynamic props in its own state and enhance the original render methods like so.

import React from 'react'
import partialRight from 'lodash/partialRight';
import ModalContainer from 'components/ModalContainer';

class ErrorModalContainer extends React.Component {
  state = { message: '' }

  onError = (message, callback) => {
    this.setState(
      () => ({ message }),
      () => callback && callback()
    );
  }

  renderModal = (props) => (
    this.props.renderModal({
       ...props,
       message: this.state.message,
    })
  )

  renderTrigger = (props) => (
    this.props.renderTrigger({
      openModal: partialRight(this.onError, props.openModal)
    })
  )

  render() {
    return (
      <ModalContainer
        renderModal={this.renderModal}
        renderTrigger={this.renderTrigger}
      />
    )
  }
}

ErrorModalContainer.propTypes = (
  ModalContainer.propTypes
);

export default ErrorModalContainer;

Update: React 16.0 introduced portals through ReactDOM.createPortal link

Update: next versions of React (Fiber: probably 16 or 17) will include a method to create portals: ReactDOM.unstable_createPortal() link


Use portals

Dan Abramov answer first part is fine, but involves a lot of boilerplate. As he said, you can also use portals. I'll expand a bit on that idea.

The advantage of a portal is that the popup and the button remain very close into the React tree, with very simple parent/child communication using props: you can easily handle async actions with portals, or let the parent customize the portal.

What is a portal?

A portal permits you to render directly inside document.body an element that is deeply nested in your React tree.

The idea is that for example you render into body the following React tree:

<div className="layout">
  <div className="outside-portal">
    <Portal>
      <div className="inside-portal">
        PortalContent
      </div>
    </Portal>
  </div>
</div>

And you get as output:

<body>
  <div class="layout">
    <div class="outside-portal">
    </div>
  </div>
  <div class="inside-portal">
    PortalContent
  </div>
</body>

The inside-portal node has been translated inside <body>, instead of its normal, deeply-nested place.

When to use a portal

A portal is particularly helpful for displaying elements that should go on top of your existing React components: popups, dropdowns, suggestions, hotspots

Why use a portal

No z-index problems anymore: a portal permits you to render to <body>. If you want to display a popup or dropdown, this is a really nice idea if you don't want to have to fight against z-index problems. The portal elements get added do document.body in mount order, which means that unless you play with z-index, the default behavior will be to stack portals on top of each others, in mounting order. In practice, it means that you can safely open a popup from inside another popup, and be sure that the 2nd popup will be displayed on top of the first, without having to even think about z-index.

In practice

Most simple: use local React state: if you think, for a simple delete confirmation popup, it's not worth to have the Redux boilerplate, then you can use a portal and it greatly simplifies your code. For such a use case, where the interaction is very local and is actually quite an implementation detail, do you really care about hot-reloading, time-traveling, action logging and all the benefits Redux brings you? Personally, I don't and use local state in this case. The code becomes as simple as:

class DeleteButton extends React.Component {
  static propTypes = {
    onDelete: PropTypes.func.isRequired,
  };

  state = { confirmationPopup: false };

  open = () => {
    this.setState({ confirmationPopup: true });
  };

  close = () => {
    this.setState({ confirmationPopup: false });
  };

  render() {
    return (
      <div className="delete-button">
        <div onClick={() => this.open()}>Delete</div>
        {this.state.confirmationPopup && (
          <Portal>
            <DeleteConfirmationPopup
              onCancel={() => this.close()}
              onConfirm={() => {
                this.close();
                this.props.onDelete();
              }}
            />
          </Portal>
        )}
      </div>
    );
  }
}

Simple: you can still use Redux state: if you really want to, you can still use connect to choose whether or not the DeleteConfirmationPopup is shown or not. As the portal remains deeply nested in your React tree, it is very simple to customize the behavior of this portal because your parent can pass props to the portal. If you don't use portals, you usually have to render your popups at the top of your React tree for z-index reasons, and usually have to think about things like "how do I customize the generic DeleteConfirmationPopup I built according to the use case". And usually you'll find quite hacky solutions to this problem, like dispatching an action that contains nested confirm/cancel actions, a translation bundle key, or even worse, a render function (or something else unserializable). You don't have to do that with portals, and can just pass regular props, since DeleteConfirmationPopup is just a child of the DeleteButton

Conclusion

Portals are very useful to simplify your code. I couldn't do without them anymore.

Note that portal implementations can also help you with other useful features like:

  • Accessibility
  • Espace shortcuts to close the portal
  • Handle outside click (close portal or not)
  • Handle link click (close portal or not)
  • React Context made available in portal tree

react-portal or react-modal are nice for popups, modals, and overlays that should be full-screen, generally centered in the middle of the screen.

react-tether is unknown to most React developers, yet it's one of the most useful tools you can find out there. Tether permits you to create portals, but will position automatically the portal, relative to a given target. This is perfect for tooltips, dropdowns, hotspots, helpboxes... If you have ever had any problem with position absolute/relative and z-index, or your dropdown going outside of your viewport, Tether will solve all that for you.

You can, for example, easily implement onboarding hotspots, that expands to a tooltip once clicked:

Onboarding hotspot

Real production code here. Can't be any simpler :)

<MenuHotspots.contacts>
  <ContactButton/>
</MenuHotspots.contacts>

Edit: just discovered react-gateway which permits to render portals into the node of your choice (not necessarily body)

Edit: it seems react-popper can be a decent alternative to react-tether. PopperJS is a library that only computes an appropriate position for an element, without touching the DOM directly, letting the user choose where and when he wants to put the DOM node, while Tether appends directly to the body.

Edit: there's also react-slot-fill which is interesting and can help solve similar problems by allowing to render an element to a reserved element slot that you put anywhere you want in your tree


A lot of good solutions and valuable commentaries by known experts from JS community on the topic could be found here. It could be an indicator that it's not that trivial problem as it may seem. I think this is why it could be the source of doubts and uncertainty on the issue.

Fundamental problem here is that in React you're only allowed to mount component to its parent, which is not always the desired behavior. But how to address this issue?

I propose the solution, addressed to fix this issue. More detailed problem definition, src and examples can be found here: https://github.com/fckt/react-layer-stack#rationale

Rationale

react/react-dom comes comes with 2 basic assumptions/ideas:

  • every UI is hierarchical naturally. This why we have the idea of components which wrap each other
  • react-dom mounts (physically) child component to its parent DOM node by default

The problem is that sometimes the second property isn't what you want in your case. Sometimes you want to mount your component into different physical DOM node and hold logical connection between parent and child at the same time.

Canonical example is Tooltip-like component: at some point of development process you could find that you need to add some description for your UI element: it'll render in fixed layer and should know its coordinates (which are that UI element coord or mouse coords) and at the same time it needs information whether it needs to be shown right now or not, its content and some context from parent components. This example shows that sometimes logical hierarchy isn't match with the physical DOM hierarchy.

Take a look at https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example to see the concrete example which is answer to your question:

import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
  const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
  return (
    <Cell {...props}>
        // the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
        <Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
            hideMe, // alias for `hide(modalId)`
            index } // useful to know to set zIndex, for example
            , e) => // access to the arguments (click event data in this example)
          <Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
            <ConfirmationDialog
              title={ 'Delete' }
              message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
              confirmButton={ <Button type="primary">DELETE</Button> }
              onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
              close={ hideMe } />
          </Modal> }
        </Layer>

        // this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
        <LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
          <div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
            <Icon type="trash" />
          </div> }
        </LayerContext>
    </Cell>)
// ...

Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax Read input from a JOptionPane.showInputDialog box Disable click outside of angular material dialog area to close the dialog (With Angular Version 4.0+) How can I display a modal dialog in Redux that performs asynchronous actions? Angular 2.0 and Modal Dialog How to use Bootstrap modal using the anchor tag for Register? Bootstrap modal opening on page load Trying to make bootstrap modal wider How to present a modal atop the current view in Swift Send parameter to Bootstrap modal window? Set bootstrap modal body height by percentage

Examples related to redux

TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received type undefined raised when starting react app Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop Receiving "Attempted import error:" in react app Local package.json exists, but node_modules missing What is the best way to redirect a page using React Router? How do I fix "Expected to return a value at the end of arrow function" warning? ReactJS lifecycle method inside a function Component How to overcome the CORS issue in ReactJS Attach Authorization header for all axios requests React - Display loading screen while DOM is rendering?

Examples related to react-redux

Local package.json exists, but node_modules missing What is {this.props.children} and when you should use it? axios post request to send form data React-Redux: Actions must be plain objects. Use custom middleware for async actions How do I test axios in Jest? How do I fix "Expected to return a value at the end of arrow function" warning? React-router v4 this.props.history.push(...) not working How to use onClick event on react Link component? How do I add an element to array in reducer of React native redux? How to enable file upload on React's Material UI simple input?