Blog Hero Background

Portals with Context


User Avatar
Brad WestfallSay hi on Twitter
October 21, 2019
ReactBeginner

See Our Public Workshops:

It seems like React's context is starting to become much more widely adopted, especially since the hook useContext makes it much easier now to consume your context. Some are even replacing their state management strategies (Redux, MobX, etc) with context. For this article, I'll assume you have the basics of context down already. If you don't, the docs are a great place to start.

Portals on the other hand are probably a lesser used feature by most React developers, but they're amazing when you need them. Depending on your circumstances, portals might be a perfect solution that allow you to migrate to React in small chunks or to use React along with your existing server-rendered CMS content. In any case, I think you should consider using context to communicate data between these separate areas in your DOM that are created through React portals.

What exactly is a portal?

In most React apps, you would probably use ReactDOM.createRoot() once. Then if any of your react components change, React will automatically flush those changes out to the real DOM.

// Once per app
const root ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
<html lang="en">
<head></head>
<body>
<div id="root">React will build the App and all its descendants here</div>
<script src="./path/to/app.js"></script>
</body>
</html>

One of React's strengths is its declarative nature - the fact that we don't have to wire up each component to its place in the DOM directly. Instead we can just describe what we want and React takes it from there.

Then, with React's approach to nesting components, your React component tree hierarchy will resemble that of your DOM hierarchy -- which is one of the main ideas in React in the first place.

"Yes, Brad, we know. What's your point?"

My point is that it's so conventional in React to just mount the app to one place with a component hierarchy that builds all your DOM, that I can imagine why someone might think this is the only way it can be done with React.

However, with portals, any component can send DOM instructions out to a different mount point instead of returning instructions to its parent. Let's do this with a Single Page App and modals just to warm up to this idea.

This is our starting DOM from the server. Notice the extra mounting point:

<html lang="en">
<head></head>
<body>
<div id="root"></div>
<div id="modal-root"></div>
<script src="./path/to/app.js"></script>
</body>
</html>

In React, we still render <App /> to the "root" div, but notice the code for the modal doesn't simply return JSX, instead it creates a portal:

function UserProfile() {
return (
<div class="user-profile">
<h1>Brad's Profile</h1>
<button>Remove User</button>
<Modal />
</div>
)
}
function Modal() {
return ReactDOM.createPortal(
<div class="modal">
<p>Are you sure...</p>
<button>Yes</button>
<button>No</button>
</div>,
document.getElementById('modal-root')
)
}
function App() {
return <UserProfile />
}
const root ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)

Normally we might expect the modal to be rendered as a sibling to the "Remove User" button since that's how it looks in UserProfile. But now we can think about it this way: The UserProfile "owns" the modal element and can pass props in and can even decide when modal mounts conditionally -- but the modal itself determines where its DOM goes.

A key takeaway here is that our React app is still a single React component hierarchy. It's just that now our component hierarchy doesn't match the DOM hierarchy. Notice that we didn't have to make two unrelated React component hierarchies to do this. We'll cover this again down below.

When portals came out, the first use-case I thought of was for modals (lightboxes, dialogs, etc). There are some styling considerations that make modals easier if they're hoisted to the top of the DOM (outside the app) instead of inline where it was created from a component hierarchy standpoint.

However, not everyone has a setup where React builds and controls all the DOM and mounts the whole thing to <div id="root"></div>. Maybe you're migrating from another JS strategy to React and you want to do so incrementally. Or maybe there's a CMS involved like WordPress which returns a bunch of HTML and we only want to use React for a few areas in the DOM but not the whole thing. Either way, portals might be a big help.

Not working with an SPA?

Let's say you're working with server-rendered content from something like WordPress. The server responds with HTML and we just want React to control certain areas, not the whole thing. If we're in the mindset where a the React component hierarchy needs to match the DOM, then I can imagine why someone might explore an idea like this where two tree structures are made:

import React from 'react'
import ReactDOM from 'react-dom'
import AppTreeOne from './AppTreeOne'
ReactDOM.createRoot(document.getElementById('root-one')).render(<AppTreeOne />)
import AppTreeTwo from './AppTreeTwo'
ReactDOM.createRoot(document.getElementById('root-two')).render(<AppTretTwo />)

This works great until this question comes up: "how do these two trees communicate?" I've seen home-grown solutions to that as well. Not that there's problems with home-grown solutions, but why bother when React has a built-in solution that's already tested and mature? (hint, it's context)

React has a great uni-directional data-flow model for communication between components, but it relies on those components to be in a single component hierarchy (one tree). So if we create one tree, we can use portals to mount our DOM at various places all while keeping communication between components a lot easier.

Let's build a quick WordPress mock example.

  • Static content from the server is shown in gray.
  • The parts React controls is shown in blue.

The main goal here is to let two components that are separated in the DOM communicate with each other:

See Example

Here's the main HTML we might receive from the server. Notice the scattered empty divs ready to be different mounting points for React:

<!-- React App Still Mounts here-->
<div id="reactRoot" style="display: none;" hidden></div>
<!-- WordPress Static Content -->
<div class="wordpress-website">
<header>WordPress Website</header>
<main>
<aside>
<div>WP Stuff</div>
<div id="startMessageRoot"></div>
<div>WP Stuff</div>
</aside>
<div class="primary-content">
<div id="messageBoxRoot"></div>
<article>WP Article</article>
</div>
</main>
</div>

Notice that there are three mount points for React:

  1. #reactRoot - A place to mount our <App /> to get things started.
  2. #startMessageRoot - A place for portals to mount to (blue box in example).
  3. #messageBoxRoot - A place for portals to mount to (blue box in example).

The idea is to mount our React app to the "reactRoot" div but I'm not going to mount any actual DOM there. React just needs a place to mount so it can get started. Then all the components in our app will use portals to render their DOM to the other mount points -- so that way <App /> will never produce DOM in the "reactRoot" div.

Here's the React code, notice how the three mounting points are being used:

const AppState = React.createContext()
function MessageBox() {
const { message } = useContext(AppState)
return React.createPortal(
<div>
<p>{message}</p>
{/* etc.*/}
</div>,
document.getElementById('messageBoxRoot')
)
}
function SendMessage() {
const { setMessage } = useContext(AppState)
return React.createPortal(
<button onClick={() => setMessage('Hello From Sidebar')}>Send Message</button>,
document.getElementById('sendMessageRoot')
)
}
function App() {
const [message, setMessage] = useState()
return (
<AppState.Provider
value={{
message,
setMessage,
clearMessage: () => setMessage(null),
}}
>
<MessageBox />
<SendMessage />
</AppState.Provider>
)
}
const root ReactDOM.createRoot(document.getElementById('reactRoot'))
root.render(<App />)

Remember, the main goal here is to get the two React areas to communicate. When components need to communicate with each other that aren't in a direct parent/child hierarchy, the docs say to lift state up to a nearest common ancestor. We can do that with props, but in this example we'll use context so you can see how this might work for bigger, more complex situations where you might not want to use props.

In our example, App is a context provider and MessageBox and SendMessage are context consumers. In order for context to work, our React component hierarchy needs to be such that the consumers are decedents of the provider. This is why it's important to keep our React application as one tree structure even though it doesn't mirror the DOM tree structure.

Thank you for reading.


Instead of having comments here in our blog, we've tweeted about this post so you can comment there if you wish.

View on Twitter

Photo by Hal Gatewood on Unsplash

Subscribe for updates on public workshops and blog posts

Don't worry, we don't send emails too often.

i love react
© 2024 ReactTraining.com