One of React’s best features is the simplicity of it’s API. A component can be as simple as a render
method which returns the component’s structure – just write a simple function, and you have a useful, re-usable piece of code. There are times where this can be limiting, however. In particular, the fact that this API gives a component no control over where in the DOM it will be mounted makes popup components (like modals or menus) difficult to implement. Problems arise when a parent element has overflow
set to 'hidden'
, for example, causing something a little like this:
When what we really want is this: Luckily, there is a fairly elegant way around this – although it isn’t glaringly obvious. One of the very first React functions that anybody will learn is React.render
, with the signature:
ReactComponent render(
ReactElement element,
DOMElement container,
[function callback]
)
Usually, we’d use this to render the entire application to a single DOM element. Happily, it isn’t limited to this. In fact, we can use
React.render
within one component to render another component to a completely different DOM element. As a component’s render
function must be pure (i.e. it cannot change state or interact with the DOM), we should instead call React.render
from componentDidUpdate
or componentDidMount
. Additionally, we need to make sure that any rendered component is properly unmounted when our parent component unmounts.
Putting it all together, we can build a component to solve this for us:
var RenderInBody = React.createClass({
componentDidMount: function() {
this.popup = document.createElement("div");
document.body.appendChild(this.popup);
this._renderLayer();
},
componentDidUpdate: function() {
this._renderLayer();
},
componentWillUnmount: function() {
React.unmountComponentAtNode(this.popup);
document.body.removeChild(this.popup);
},
_renderLayer: function() {
React.render(this.props.children, this.popup);
}
render: function() {
// Render a placeholder
return React.DOM.div(this.props);
}
});
Then, whenever we want to escape our parent component’s DOM and render directly to document.body
, all we need to do is wrap our component’s output in <RenderInBody>
, like so:
var Dialog = React.createClass({
render: function() {
var dialogPopup = <DialogPopup {...this.props} />;
return (
<RenderInBody>{dialogPopup}</RenderInBody>
);
}
});
If you value your time and want to create Single Page Applications which people love, subscribe to my newsletter! In return for your e-mail, you’ll also immediately receive 3 bonus print-optimised PDF cheatsheets – on React, ES6 and JavaScript promises.
Nice post James! My team wanted to do something like this so we could add popover/drop-down options to components without worrying if all their containing elements had the proper CSS styles (overflow visible, relative position, etc). We decided it would be more consistent to render straight to the document body and position the popovers with javascript.
Although this is a relatively old post (in React time), because we used this code sample as a starting point for our solution, I wanted to point out one small, but consequential pitfall of the approach:
When you call
React.render(this.props.children, this.popup);
every time, you miss out on many of the great parts of React like the virtual DOM, it’s speed, and the diffing that occurs when the component is re-rendered into the DOM.The approach we settled on involves saving a reference to the rendered component when we render it into the DOM on
componentDidMount
. We can thensetState
whenever the component receives updates, and utilize the power of React’s virtual DOM! Only the elements that have changed will be updated.componentDidMount: function () {
this._container = document.createElement("div");
document.body.appendChild(this._container);
this._child = React.render(this.props.children, this._container);
},
componentDidUpdate: function () {
if (!this._child) return;
this._child.setState({});
},
componentWillUnmount: function () {
React.unmountComponentAtNode(this._container);
document.body.removeChild(this._container);
}
Works like a charm. Integration remains exactly the same as the code you posted.
In other news, I also learned today that you can return
null
from a component’s render function instead of an empty div! React will render an empty noscript tag into the DOM instead of the empty div, so you can be completely sure it does nothing (:render: function () {
return null;
}
Thanks again!
This didn’t seem to work with your componentDidUpdate function. I had to put the ReactDom.render back in.
I am also experiencing issues with the componentDidUpdate() here. It seems the example works excellent up to the point where if something updates. My props aren’t getting passed down into the child component. Is this what you were seeing also? What did you have to do to fix this?
This is what I ended up doing in order to get it to update my child component with the new props, however it won’t be using the virtual dom anymore with this way. Does anyone have ideas on how we can get this to work the right way? The componentDidUpdate is where my main concerns lie. Everything else seems to work fine from what I can tell.
export class RenderInBody extends React.Component {
private container: Element;
private child: any;
componentDidMount() {
this.container = document.createElement(“div”);
document.body.appendChild(this.container);
this.child = ReactDOM.render(this.props.children, this.container);
}
componentDidUpdate() {
if (!this.child) return;
this.child = ReactDOM.render(this.props.children, this.container);
this.child.setState({});
}
componentWillUnmount() {
ReactDOM.unmountComponentAtNode(this.container);
document.body.removeChild(this.container);
}
render() {
return null;
}
}
Hi, this solution definitely looks like what I need to handle dialogs.
But I don’t really get it… what should the dialog do to unmount the RenderInBody component?
In other words, how do we get to have “componentWillUnmount” called?
Thanks!
Ok I was misunderstanding. This piece of code is to make sure that the child component (popup) is destroyed when the parent unmounts. Not as I thought a helper to create/destroy only the popup. But is will be a good basis for this purpose anyhow.
Thanks for this post.
Hi, I’m curious what benefits/drawbacks this has compared to using CSS like position: fixed?
sometimes that could be not an option
This is great and it seems like a work around to render to a non React dom node generated by js.
also saw this one :
https://github.com/Khan/react-components/blob/master/js/layered-component-mixin.jsx