How to Manage the Entire Body in React.js
May 19, 2020 • 6 Minute Read
Introduction React is a vast library that abstracts all the complexities of the DOM. It uses the virtual DOM concept, which is much faster than direct DOM manipulation for large and nested elements. Each React component represents a node in the DOM. By design, components do not talk up the component hierarchy directly; you have to pass the data using callback props.
But what if there's a DOM node that your React app wants to reach and modify something it doesn't control? In this short guide, you'll learn how to manage the entire body, including the <body> element in React.
Using ReactDOM.render() Method
The react-dom library provides DOM-specific utilities. The ReactDOM.render() method renders a React component into the DOM in the specified DOM element. It takes in a React element as the first parameter and a DOM node as the second. It also takes an optional callback as a third parameter, which is executed when the component has finished rendering.
ReactDOM.render(component, DOMnode [, callback]);
You can render a component in the body as follows.:
const App = () => (
<div>
<h1>Hello World</h1>
</div>
);
ReactDOM.render(<App />, document.body);
This is a possible solution, but it is considered bad practice to render a component to document.body. That is because any other third party library or browser extension can manipulate document.body and its children, which may affect the React component rendered inside of it.
You could do document.body.appendChild(document.createElement("div")), but this will render the component inside of a direct child of the body.
Higher-Order Component to Manipulate the <body /> Element
In this section, you'll look into a more "React" way of handling the <body> element.
Developers often forget that they can use Browser DOM API inside of a React component. When a React component manipulates the DOM node directly, it is considered as a side effect. Nevertheless, it's perfectly valid.
So to add attributes or classes to the <body> element, you can do:
// to set new class
document.body.classList.add(className);
// to set attributes
document.body.setAttribute(attrName, value);
The above methods are specified by the DOM API to manipulate DOM nodes, and are not a part of core React or framework agnostic.
Using the above methods, create a Higher Order Component (HOC) to add classes and set data attributes in the <body> element.
import React, { Component } from "react";
class ReactBody extends Component {
render() {
return props.children;
}
}
The ReactBody component will take in the classes and attributes as props. The following is the propTypes definition for the component:
import PropTypes from "prop-types";
ReactBody.propTypes = {
classes: PropTypes.array,
dataAttrs: PropTypes.object,
children: PropTypes.node.isRequired
};
Next, inside the componentDidMount() lifecycle method, loop through each of the class names and attributes, and add them to the <body> element.
// ...
componentDidMount() {
if (props.classes) {
props.classes.forEach(class => {
document.body.classList.add(class);
})
}
else if (props.dataAttrs) {
Object.keys(props.dataAttrs).forEach(attr => {
document.body.setAttribute(`data-${attr}`, props.dataAttrs[attr]);
});
}
}
// ...
Don't forget to remove the attributes and class name after the component gets unmounted from DOM.
// ...
componentWillUnmount() {
if (props.classes) {
props.classes.forEach(class => {
document.body.classList.remove(class);
})
}
else if (props.dataAttrs) {
Object.keys(props.dataAttrs).forEach(attr => {
document.body.removeAttribute(`data-${attr}`, props.dataAttrs[attr]);
});
}
}
// ...
Finally, your Higher-Order Component should look as follows:
import React, { Component } from "react";
import PropTypes from "prop-types";
class ReactBody extends Component {
componentDidMount() {
if (props.classes) {
props.classes.forEach(class => {
document.body.classList.add(class);
})
}
else if (props.dataAttrs) {
Object.keys(props.dataAttrs).forEach(attr => {
document.body.setAttribute(`data-${attr}`, props.dataAttrs[attr]);
});
}
}
componentWillUnmount() {
if (props.classes) {
props.classes.forEach(class => {
document.body.classList.remove(class);
})
}
else if (props.dataAttrs) {
Object.keys(props.dataAttrs).forEach(attr => {
document.body.removeAttribute(`data-${attr}`, props.dataAttrs[attr]);
});
}
}
render() {
return this.props.children
}
}
ReactBody.propTypes = {
classes: PropTypes.array,
dataAttrs: PropTypes.object,
children: PropTypes.node.isRequired
};
export default ReactBody;
Once you have your ReactBody HOC ready, you can wrap other components inside of <ReactBody /> as shown below.
// ...
const Modal = props => {
<ReactBody classes={props.isOpen && ["open-modal"]}>
<div className="modal">{/* ... */}</div>
</ReactBody>;
};
Conclusion
It's highly recommended that you never render your React app directly inside the <body> element, as it can lead to unexpected behaviors. A better way to manipulate the <body> is to use DOM methods like classList.add, setAttribute, etc. To reuse these methods across React components, you should create a HOC that does all the manipulation to the document body. If you still have any questions, contact me at CodeAlphabet.