Building a simple and reusable modal with React hooks and portals

A lot of interesting new things have been added to React in the last two years. For me personally, hooks have probably been the most exciting addition in recent years. Before hooks I pretty much always used recompose, a simple helper library consisting of some higher-order components to wrap functional components with state or lifecycle access.

Now that we have hooks, it is easier than ever to add state or side effects to a functional react component. To highlight this I will show you how you can build a simple general-purpose useModal() hook. We will also make sure that our modal will always be rendered into a specific DOM node. So no matter where we use the modal inside our react component tree, it will be a direct descendant of a predefined div.

Without further ado let's dive right in:

The skeleton

To build a small React app we first need our basic skeleton. I assume that you are familiar with the basics of React and have some understanding of how a very basic React app is structured. So I skip the standard index.html file here, as we won't do anything out of the ordinary with it.

So our basic skeleton is just a plain index.tsx file consisting of our ReactDOM.render() call with our main <App/> component. We will soon have a closer look at this component, but first I would like to create our actual <Modal />

import React from 'react' import ReactDOM from 'react-dom' import App from './App' ReactDOM.render( <App />, document.getElementById('root') )

The modal

Below you see the code we use for our very simple modal. We won't style our modal or do other fancy stuff during this tutorial, as its main purpose is to showcase some implementation details.

So let's have a look at the API of our modal. We expect a closeModal() callback function which will be triggered whenever our close button is pressed. Apart from that we just render any children passed to our modal inside our div.

You might already have guessed what the whole ReactDOM.createPortal() thing around our div is. Like ReactDOM.render() createPortal() allows us to define a specific DOM-element where our component will always be rendered to. In this case, our DOM element should exist and have the id attribute "modal-root". We will soon define this element inside our main <App/> component, so bear with me ?

Note: The wrapping call to React.memo() is an optimization which is fairly similar to extending React.PureComponent in good old class components.

import React from 'react' import ReactDOM from 'react-dom' type Props = { children: React.ReactChild closeModal: () => void } const Modal = React.memo(({ children, closeModal }: Props) => { const domEl = document.getElementById('modal-root') if (!domEl) return null // This is where the magic happens -> our modal div will be rendered into our 'modal-root' div, no matter where we // use this component inside our React tree return ReactDOM.createPortal( <div> <button onClick={closeModal}>Close</button> {children} </div>, domEl ) }) export default Modal

Defining our custom useModal hook

While we have already created a reusable component which will render to a specific DOM node, we haven't built the actual modal functionality yet. To open and close our modal we do need some form of state. Let's model our state as an isVisible boolean.

To do this we import the useState() hook from React and call it inside our functional component with a default value of false. useState() returns an array which holds the current state value at index 0 and a function to transform the state at index 1. Thanks to ES6s beautiful destructuring syntax, we can simply destructure both values into their respective variable (see line 12 below). We also define two convenience methods show() and hide() to make our state transformations even more explicit and readable.

The last part of our custom hook is our <RenderModal/> component. This component hands any children we pass to it to the <Modal/> component we defined earlier. It will also only render them if our modal is visible. Because we created an enclosing scope around our render modal, we also have access to our hide() function, which we pass to the closeModal-prop of our <Modal/>-Component.

Last but not least, we return 

  • an object from our hook, containing the show() function, which has to be called outside of our modal,
  • the hide() function (in case we want to add additional triggers to close the modal) and
  • our <RenderModal/>-component.

We could also have exported our hook elements inside an array, like useState() does, but having actually named properties gives us a bit more flexibility when consuming our hook. You will soon see what this looks like.

import React, { useState } from 'react' import Modal from './Modal' // Renders a modal to the modal root and handles the visibility state // of this modal. // // NOTE: Each modal you want to render should use a separate hook!!! // Otherwise your modals will share their visibility state which might lead // to overlapping and unclosable elements. export const useModal = () => { const [isVisible, setIsVisible] = useState(false) const show = () => setIsVisible(true) const hide = () => setIsVisible(false) const RenderModal = ({ children }: { children: React.ReactChild }) => ( <React.Fragment> {isVisible && <Modal closeModal={hide}>{children}</Modal>} </React.Fragment> ) return { show, hide, RenderModal, } }

Using the hook

Now that we have everything in place, we can build our actual app. Our <App/> consists of two main parts:

1. A <div> containing a button to open our <Modal/>, and our <RenderModal/> component
2. Another <div> which has our 'modal-root'-id. This is where all our modals will be rendered to.

At line 6 we use our custom useModal() hook. Note that we just destructure show() and <RenderModal/> by using the object destructuring syntax. Here it comes in handy, that our hook returns an object instead of an array, as it allows us to easily specify which parts of our hooks we need, by naming them.

Our show() function is now passed to the onClick() handler prop of our <button/>. Our <RenderModal> component, on the other hand, can be used like any other React component. In our example, we pass a <p>-tag with some text as children to it.

import React from 'react' import { useModal } from './useModal' const App = React.memo(() => { const { show, RenderModal } = useModal() // we could also spread 'hide' here, if we somehow needed it outside of the modal return ( <div> <div> <p>some content...</p> <button onClick={show}>MODAL anzeigen!</button> <RenderModal> <p>This is stuff which will be rendered inside our 'modal-root' div.</p> </RenderModal> </div> <div id='modal-root' /> </div> ) }) export default App

Rendering multiple modals

We can even render multiple modals this way. Because we abstracted our whole component state away into a reusable hook, we can just call it twice to create our respective components and handler functions (see line 6 and 7).

import React from 'react' import { useModal } from './useModal' const App = React.memo(() => { const { show: showA, RenderModal: RenderModalA } = useModal() // note how we alias our destructured variables by providing a name behind the colon const { show: showB, RenderModal: RenderModalB } = useModal() return ( <div> <div> <p>some content...</p> <button onClick={showA}>Modal A zeigen</button> <button onClick={showB}>Modal B zeigen</button> <RenderModalA> <p>Content of A</p> </RenderModalA> <RenderModalB> <p>Content of B</p> </RenderModalB> </div> <div id='modal-root' /> </div> ) }) export default App

Wrapping it up

We created a simple and re-usable modal solution with React hooks and portals. This component can be easily enhanced. We could, for example, make our <Modal/>-component more complex to use a title- and a body-prop instead of just passing it children. This way we could have a predefined layout, which would be used by every modal. We could also export the isVisible-state from our useModal() hook and use it inside our surrounding application (e.g. to show an additional hint only when the modal is visible).

I hope you are as inspired by hooks as I am. Thanks for reading and happy coding =)