Using React portals
IN: REACTJSThis will be a short story about React Portals. What was my use case and how I implemented a portal.
We will see that the way I used it is tightly coupled with GatsbyJS. If you don't know what GatsbyJS is, I strongly recommend that you get familiar. In fact, the whole blog is build thanks to Gatsby. It was really fun and worth playing with it, while the result is a really blazing fast site. Am I lying?
Let's not get lost in things, not relevant to this article. Maybe I'll write another post, specific to building a blog with GatsbyJS.
Portals? Can I eat it?
So back to our main subject. Portals come packed with React by default since the official release of version 16.0. Portals make it possible to render react components or plain jsx in places, which are not direct descendants of the current parent node. Simply said - we can send an element outside our current hierarchy.
Sounds easy and you might already be thinking "Why the hell is this even a thing?". Well, everything has it's reason, it does.
Let's look at some syntax:
import ReactDOM from 'react-dom'
...
ReactDOM.createPortal(children, domElement)
Where children
is a regular react component, string or fragment and domElement
is the element from the DOM node, where we want our children to be rendered.
This is actually all we need to do to send content outside our current parent and nested children. This may sound very confusing when you first come into using it, because it looks like it breaks the react idea of a parent encapsulating it's children. But, another interesting thing about portals is that events fired from within them, bubble up to the parent where they are used, not where you have send them. Which means that they still remain tightly coupled with their parents and parents can still control children, who are portals.
When do we need them
There are many situations, where implementing something like portals will help you do certain things. I shall give you an insight about my use case.
From the very beginning I knew you had a keen eye. So I assume you haven't missed this part of the page:
Yes, these fancy little guys. Those buttons, which I indifferently advice you to press right now and go further along their way, are actually "ported" to where you see them. I used react portal to achieve this and I shall show you why and how.
I already mentioned that this blog is made with GatsbyJS. Well, Gatsby gives me the opportunity to have a layout component, which renders all other components as its children props. To use this layout component, since Gatsby v2, all inner components must be wrapped inside it like this - <Layout>...</Layout>
.
What I wanted from the UI was a three column page: first column to hold the social share buttons we talk about, middle one to render all posts or a specific one and the third column to have the categories and tags menu. You can see the last column when you open the site on a bigger screen. Basically, my layout component looks something similar to this:
<section class='root'>
<Grid>
<GridItem className='page-left'>
<section id='social-share'></section>
</GridItem>
<GridItem className='page-middle'>
{children}
</GridItem>
<GridItem className='page-right'>
<RightMenu />
</GridItem>
</Grid>
</section>
Where every GridItem
represents one of the three columns. The section
DOM element will be used to create the portal via it's self-descriptive id - social-share
. If you still wonder why I need this, don't worry, it will become clearer in a minute.
What I wanted was a persistent view of the share buttons. However, when you open a page, where you browse all posts for example, you are not bound to a certain post to share it's link. So, what I was looking to avoid was having to show these buttons only when you open a post. When you are browsing posts and wondering what to read, share button's links simply point toward the domain of this site.
However, to achieve this the react way, the social share buttons component must be a descendant of the post component. Because, that's where I fetch data and have the post link. Or, the link must be passed on by some state management library to the component, where needed. All these workarounds work like a charm, but for something simple like this I needed an easy getaway. This is where I needed react portals.
My portal does what my portal does
What I did was plain simple - created a reusable portal component. Well, not as reusable as it can be. You can play with it more and make the id of the portal element (social-portal
) to be passed on via props. This way you can render any component, wherever you want. Ladies and gentleman, may I present you the component itself:
import { Component } from 'react'
import ReactDOM from 'react-dom'
export default class SocialPortal extends Component {
constructor(props) {
super(props)
this.state = {
el: null,
isLoad: false
}
}
componentDidMount() {
const element = document.createElement('div')
this.setState({
el: element,
isLoad: true
}, () => {
const portalRoot = document.getElementById('social-share')
portalRoot.appendChild(this.state.el)
})
}
componentWillUnmount() {
const portalRoot = document.getElementById('social-share')
portalRoot.removeChild(this.state.el)
}
render() {
if(this.state.isLoad){
const { children } = this.props
return ReactDOM.createPortal(children, this.state.el)
}
return null
}
}
It would be a lot shorter source code, if building sites didn't exist. Imagine a world where all we had to do was run it in development mode...Ah, such a lovely place. Sadly, that's not how things work out. When gatsby builds it's pages, it has a thing that executing this line const portalRoot = document.getElementById('social-share')
comes before actually building the whole DOM. And guess what? When doing that, the div with id social-share
is not rendered yet . Pain... The result is a crashing build. Yet, in development there is no issue. So, to escape this error, I had to make sure everything is loaded before trying to render the portal. This is why I query the document on componentDidMount
lifecycle. When the component is not loaded I return null
and everything works smoothly.
The princess was saved, they lived happily ever after blah-blah-blah...
Yeah, right, but someone or something has to clean up after all this. This is why it is so necessary to have the portal component destroy the created element, when it is no longer needed. If you haven't figured out how to yet, this happens in the componentWillUnmount
lifecycle method.
So, when all is said and done, we are set to use the portal wherever we want. In my postTemplate.js component I render the portal like this:
...
<SocialPortal>
<SocialShare url={this.postURL} title={this.postTitle} excerpt={this.postExcerpt} />
</SocialPortal>
As you can see, this way makes it possible to send data to the social share component via props, without moving the state up or using state management machines.
On the other hand, in my allPosts.js component I can use it like this:
...
<SocialPortal>
<SocialShare />
</SocialPortal>
This was a short tutorial of how to implement react portal. Actually I didn't find much examples on the internet and that is the reason I decided to write this post. I hope you liked it, if not, at least you read it .
Go and tell your friends about it.
Don't hesitate to contact me to report any mistakes or just to say hi.