Mastering Modals in React with Context and Portals

  • Publish Date
  • Reading Time
    4 minutes
  • Tags
    • react
Tweet

    Modals are a useful tool for displaying information on top of your application, and are often used for notifications, alerts, or standalone dialogs such as registration or login forms. Before building a custom modal, it's a good idea to check if there are any pre-existing solutions that meet your needs (Reach UI's Dialog and react-modal are both popular options). If you don't find a suitable solution, let's explore creating a bespoke modal component in React.

    To get started, we'll create a basic modal that appears and disappears based on some local state in our React app. The process is simple: when a button in the root of the app is clicked, the modal will appear. Then, when the button inside the modal is clicked, the modal will close. Let's start building!

    If you want to trigger the modal from a nested component rather than just from within

    Loading...
    , you can pass the
    Loading...
    action
    Loading...
    as a prop. Then, you can call this action as a callback when a button within the nested component is clicked, which will trigger the modal.

    This works for a single level of nesting, but it probably won’t scale very well. We could keep passing the callback down through the components, but that could get a bit tedious and create a lot of extra code that's tough to manage. Enter React Context.

    Context allows you to store and access a value anywhere in your React app. You can use a Provider to store the value and a Consumer to access it, and the Consumer will search up the component tree for the first Provider that matches its context. This is useful when you want to trigger the modal from a nested component, rather than just from the top-level App component. You can use the

    Loading...
    hook to consume the value in a nested component.

    Enjoying the article?

    Support the content

    Let’s wrap the previous example with a Provider, set the

    Loading...
    callback as its value, then utilise the useContext() hook to consume it in a nested component.

    We now have a modal that can be opened from anywhere in our app, but it can only display static content for now. If we want it to render dynamic content, we'll need to refactor it to accept children. Plus, since React's data flow only goes one way, we'll need to find a good way to pass data from a nested component back up to the modal at the root level.

    My former colleague, Jenna Smith, a highly skilled front-end developer, suggested using React Portal as a solution. Portal's are designed to pass children to a DOM node outside the hierarchy of the parent component, which is perfect for our needs. To use a portal, we'll need to provide two arguments: a React element (for our dynamic content) and a DOM element to inject the content into (the modal's container). This should allow us to effectively pass the data from the nested component to the modal at the root level.

    As demonstrated in the sandbox, Jenna created two functional components to provide dynamic content for the modal. The

    Loading...
    component includes a DOM element with a ref attached (
    Loading...
    ), as well as a context provider that wraps the entire app and distributes the ref's current value to any relevant consumers within it. The second component is the modal itself. Each time a
    Loading...
    component is rendered, it will try to retrieve the
    Loading...
    element using
    Loading...
    . If the ref exists, the component will create a React portal and inject the modal's children into the ref element, rather than mounting in its expected position in the DOM tree..

    With this approach, the

    Loading...
    component can now be used anywhere within the
    Loading...
    to display dynamic content on top of the app. One thing to keep in mind is that the body will still be able to scroll on iOS when the modal is mounted. I highly recommend reading Will Po's article on body scroll lock for potential solutions to this issue.