Category Archives: React

Implementing correct modal navigation with react-router

I’ve recently been converting a big project from jquery mobile into React and Material-UI. One area that seems pretty weak compared to other frameworks is with regards to having a proper mobile-focused infrastructure for navigation. One of the big issues for me was that of modals (for example a dialog, alert or popup page components). For example when you see an alert you want to be able to click on the ok button to dismiss it. If you are on android you also expect to be able to press the back button to dismiss. However if you have separate routes for your modals such that the url changes when they are open, if you refresh the page at that point you will have a modal but no previous state to explore. There seem to be certain hacks with react-router-dom which allow you to change the page but keep the URL the same, assuming you are using BrowserRouter, however because this was a legacy project I want to keep on using HashRouter. So, I whipped up a quick HOC hack to wrap around a modal which will allow both the back navigation to work as expected, and also for refreshes to go to the main page rather than opening the modal.

I already had a standard base class which had the handleClose() method to close off a dialog and signal to the parent that it was done, so I expanded it to include a state listener as below

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

// Shows some sort of dialog which has a handleClose method to close it,
// optionally calling props.onClose and also handles history appropriately
export const DialogMixin = Base => class extends Base {
    displayName = `withDialogMixin(${getDisplayName(Base)})`;

    __close = () => {
        if( window.location.hash == this.__start_hash ) {
            window.removeEventListener('popstate', this.__close);
            this.setState({ closed: true });
            if( this.props.onClose )
                this.props.onClose();
        }
    }

    handleClose = () => window.history.back();

    render() {
        // A component could be mounted but not showing any dialog - only hook
        // the history if actually showed something.
        const render = super.render();
        if( !this.state.closed && !this.__start_hash && render ) {
            this.__start_hash = window.location.hash;
            window.location.hash += '?dialog';

            window.addEventListener('popstate', this.__close);
        }
        return render;
    }
}

You then just wrap your component with this – for example

export const Alert = DialogMixin(_Alert);

This works because react-router-dom doesn’t attempt to parse query strings etc, so when the app is first loaded you want to just have something to remove any of the ?dialog path hacks:

window.location.hash = window.location.hash.replace(/\?.*/, '');