Creating Reusable Generic Modals in React and Redux

Mark Erikson

November 11th, 2016

Modal dialogs are a common part of user interface design. As with most other parts of a UI, modals in a given application probably fall into two general categories: modals that are specific to a given feature or task, and modals that are intended to be generic and reusable. However, defining generic reusable modal components in a React/Redux application presents some interesting challenges. Here's one approach you can use to create generic reusable modals that can be used in a variety of contexts throughout a React/Redux application.

First, we need a way to manage modals in general. In a typical object-oriented widget API, we might manually create an instance of a modal class, and pass in some kind of callback function to do something when it's closed. Here's what this might look like for a ColorPicker modal in an OOP API:

const colorPickerInstance = new ColorPicker({
    initialColor : "red",
    onColorPicked(color) {
        // do something useful with the "returned" color value
    }
});

colorPickerInstance.show();

This presents some problems, though. Who really "owns" the ColorPicker? What happens if you want to show multiple modals stacked on each other? What happens with the ColorPicker instance while it's being displayed?

In a React/Redux application, we really want our entire UI to be declarative, and to be an output of our current state. Rather than imperatively creating modal instances and calling show(), we'd really like any nested part of our UI to be able to "request" that some modal be shown, and have the state and UI updated appropriately to show the modal.

Dan Abramov describes a wonderful approach on Stack Overflow to React/Redux modal management, in response to a question about displaying modal dialogs in Redux. It's worth reading his answer in full, but here's a summary:

  • Dispatch an action that indicates you want to show a modal. This includes some string that can be used to identify which modal component should be shown, and includes any arbitrary values we want to be passed along to the rendered modal component:
    dispatch({
    	type : 'SHOW_MODAL",
    	payload : {
    		modalType : "SomeModalComponentIdentifier",
    		modalProps : {
    		    // any arbitrary values here that we want to be passed to the modal
    		}
    	}
    });
    
    
  • Have a reducer that simply stores the modalType and modalProps values for 'SHOW_MODAL', and clears them for 'HIDE_MODAL'.
  • Create a central component that connects to the store, retrieves the details ofwhat modal is open and what its props should be, looks up the correct component type, and renders it:
    import FirstModal from "./FirstModal"; 
    import SecondModal from "./SecondModal";
    
    // lookup table mapping string identifiers to component classes 
    const MODAL_COMPONENTS = { FirstModal, SecondModal };
    
    const ModalRoot = ({modalType, modalProps}) => { 
       if(!modalType) return null; 
       
       const SpecificModal = MODAL_COMPONENTS[modalType];
       
       return <SpecificModal {...modalProps} />
    }
    
    const mapState = state => state.modal; 
    
    export default connect(mapState)(ModalRoot);

From there, each modal component class can be connected to the store, retrieve any other needed data, and dispatch specific actions for both internal behavior as well as ultimately dispatching a 'HIDE_MODAL' action when it's ready to close itself. This way, the handling of modal display is centralized, and nested components don't have to "own" the details of showing a modal.

Unfortunately, this pattern runs into a problem when we want to create and use a very generic component, such as a ColorPicker. We would probably want to use the ColorPicker in a variety of places and features within the UI, each needing to use the "result" color value in a different way, so having it dispatch a generic 'COLOR_SELECTED' action won't really suffice. We could include some kind of a callback function within the action, but that's an anti-pattern with Redux, because using non-serializable values in actions or state can break features like time-travel debugging. What we really need is a way to specify behavior specific to a feature, and use that from within the generic component.

The answer that I came up with is to have the modal component accept a plain Redux action object as a prop. The component that requested the dialog be shown should specify that action as one of the props to be passed to the modal. When the modal is closed successfully, it should copy the action object, attach its "return value" to the action, and dispatch it. This way, different parts of the UI can use the "return value" of the generic modal in whatever specific functionality they need.

Here's how the different pieces look:

// In some arbitrary component:

const onColorSelected = {
    type : 'FEATURE_SPECIFIC_ACTION',
    payload : {
        someFeatureSpecificData : 42,
    }
};

this.props.dispatch({
     type : 'SHOW_MODAL",
     payload : {
         modalType : "ColorPicker",
         modalProps : {
           initialColor : "red",
            // Include the pre-configured action object as a prop for the modal
           onColorSelected
        }
    }
});


// In the ColorPicker component:

handleOkClicked() {
    if(this.props.onColorSelected) {
        // If the code that requested this modal included an action object,
        // clone the action, attach our "return value", and dispatch it
        const clonedAction = _.clone(this.props.onColorSelected);
        clonedAction.payload.color = this.state.currentColor;

        this.props.dispatch(clonedAction);
    }

    this.props.hideModal();
}


// In some reducer:
function handleFeatureSpecificAction(state, action) {
    const {payload} = action;
    // Use the data provided by the original requesting code, as well as the
    // "return value" given to us by the generic modal component
    const {color, someFeatureSpecificData} = payload;

    return {
        ...state,
        [someFeatureSpecificData] : {
            ...state[someFeatureSpecificData],
            color
        }
    };
}

This technique satisfies all the constraints for our problem. Any part of our application can request that a specific modal component be shown, without needing a nested component to "own" the modal. The display of the modal is driven by our Redux state. And most importantly, we can specify per-feature behavior and use "return values" from generic modals while keeping both our actions and our Redux state plain and serializable, ensuring that features like time-travel debugging still work correctly.

About the author

Mark Erikson is a software engineer living in southwest Ohio, USA, where he patiently awaits the annual heartbreak from the Reds and the Bengals. Mark is author of the Redux FAQ, maintains the React/Redux Links list and Redux Addons Catalog, and occasionally tweets at @acemarke. He can be usually found in the Reactiflux chat channels, answering questions about React and Redux. He is also slightly disturbed by the number of third-person references he has written in this bio!