Whenever you create a React project, one of the first things you need to install along with the react
package is the react-dom
package. Have you ever wondered why you need it?
It may come as a surprise, but we cannot render UI components with just the react
package. For rendering the UI to the browser, we have to use react-dom
. In this guide, we will explore managing DOM components with ReactDOM by creating a sample app. As you follow along, we will make some changes to our code to learn about the multiple methods available to you.
You can check out the code for the sample app on Github and the deployed site.
ReactDOM renders components or JSX elements to the DOM. The ReactDOM object has only a handful of methods; you’ve probably used the render()
method, which is responsible for rendering the app in the browser.
The react-dom
package serves as the entry point to the DOM. We import it at the top of our project like this:
import ReactDOM from 'react-dom';
Before we go into the details of ReactDOM’s methods, let’s first understand why we need ReactDOM instead of just using the DOM.
JavaScript and HTML can’t communicate directly with one another, so the DOM was developed to deal with this issue. When you open a webpage, the browser engine translates all of its content into a format that JavaScript can understand — a DOM tree.
The structure of this tree is identical to that of the corresponding HTML document. If one element is nested inside another in the HTML code, this will be reflected in the DOM tree.
You can see for yourself what the DOM looks like by opening the Elements tab in the developer tools panel of your browser. What you’ll see will look very similar to HTML code, except that rather than looking at HTML tags, what you’re actually seeing is elements of the DOM tree.
The DOM is the logical representation of a web page created by, and stored in, the user’s browser. The browser takes the site’s HTML and transforms it into a DOM, then paints the DOM to the user’s screen, making the site visible to the user.
Let’s see DOM in action. The diagram shows how the DOM sees the HTML:
<body> <nav> <ul> <li>Home</li> <li>Contact</li> </ul> </nav> <section class="cards"> <img src="" alt="" /> <div class="post-content"> <h1>Virtual DOM vs DOM</h4> </div> </section> </body>
The DOM has some problems, though. Imagine the user clicks on a button to remove an item. That node and all the other nodes depending on it will be removed from the DOM.
Whenever a browser detects a change to the DOM, it repaints the entire page using the new version. But do we really need to repaint the whole page? Comparing two DOMs to determine which parts have changed is very time consuming.
As a result, it’s actually faster for browsers to simply repaint the entire page whenever a user interacts with the site. That’s where the Virtual DOM comes in.
A Virtual DOM is when React creates their own representation of the DOM as a JavaScript object. Whenever a change is made to the DOM, the library instead makes a copy of this JavaScript object, makes the change to that copy, and compares the two JavaScript objects to see what has changed. It then informs the browser of these changes and only those parts of the DOM are repainted.
Making changes to JavaScript objects and comparing them is far faster than trying to do the same with DOMs. Since this copy of the DOM is stored in memory as a JavaScript object it’s called a Virtual DOM.
The Virtual DOM prevents unnecessary repaints by only repainting updated elements and groups. The VDOM is a lightweight, fast, in-memory representation of the actual DOM.
Even though React works with the VDOM whenever possible, it will still regularly interact with the actual DOM. The process by which React updates the actual DOM to be consistent with the VDOM is called reconciliation.
ReactDOM.render()
Now that we have a better understanding of the DOM and VDOM, we can start learning about our first method: ReactDOM.render
. Usage of the render method is as follows:
ReactDOM.render(element, container[, callback]) ReactDOM.render(<h1>ReactDOM</h1>, document.getElementById("app"))
The first argument is the element or component we want to render, and the second argument is the HTML element (the target node) to which we want to append it.
Generally, when we create our project with create-react-app
, it gives us a div
with the id
of a root
inside index.html
, and we wrap our React application inside this root div.
So, when we use the ReactDOM.render()
method, we pass in our component for the first argument, and refer to id="root"
with document.getElementById("root")
as the second argument:
<!DOCTYPE html> <html lang="en"> <head> <!-- ... --> </head> <body> <!-- ... --> <div id="root"></div> </body> </html>
// src/index.js import React from 'react'; import ReactDOM from 'react-dom'; // create App component const App = () => { return <div>Render Me!</div> } // render App component and show it on screen ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.render()
In June, the React team announced React 18, and with the new update, we will not be using ReactDOM.render()
anymore. Instead, we will be using ReactDOM.createRoot
.
The alpha version of React 18 is available, but it will take several months for the public beta. If you want to experiment with the React 18 alpha version, install it like this:
npm install react@alpha react-dom@alpha
With React 18, we will use ReactDOM.createRoot
to create a root, and then pass the root to the render function. When you switch to createRoot
, you’ll get all of the new features of React 18 by default:
import ReactDOM from "react-dom"; import App from "App"; const container = document.getElementById("app"); const root = ReactDOM.createRoot(container); root.render(<App />);
ReactDOM.createPortal()
Our second method on ReactDOM is createPortal
.
Do you ever need to create an overlay, or modal? React has different functions to deal with modals, tooltips, or other features of that nature. One is the ReactDOM.createPortal()
function.
To render a modal, or overlay, we need to use the z-index
property to manage the order in which elements appear on the screen. z-index
allows us to position elements in terms of depth, along the z-axis.
However, as you know, we can only render one div, with all the other elements nested inside our root div. With the help of the createPortal
function, we can render our modals outside of the main component tree. The modal will be the child of the body element. Let’s see how.
In our index.html
we will add another div for the modal:
// index.html <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <div id="modal"></div> </body>
The ReactDOM.createPortal()
function takes two arguments: the first is JSX, or what we want to render on the screen, and the second argument is a reference to the element we want to attach to our modal:
// Modal.js import { createPortal } from 'react-dom'; const modalRoot = document.querySelector('#modal'); const Modal = ({ children }) => createPortal(children, modalRoot); export default Modal;
Now, to render our component, we can pass whatever we want to show between our modal component’s opening and closing tags. This will be rendered inside the modal component as children
. I have rendered the modal inside App.js
.
In our App.js
file, we have a button to open the modal. When the user interacts with the button, we are showing the modal along with a close button:
// App.js import React, { useCallback, useState } from 'react'; import Modal from '../Modal'; const App = () => { const [showModal, setShowModal] = useState(false); const openModal = useCallback(() => setShowModal(true), []); const closeModal = useCallback(() => setShowModal(false), []); return ( <div className='App'> <button onClick={openModal} className='button node'> Click To See Modal </button> {showModal ? ( <Modal> <div className='modal-container'> <div class='modal'> <h1>I'm a modal!</h1> <button onClick={closeModal} className='close-button'></button> </div> </div> </Modal> ) : null} </div> ); }; export default App;
ReactDOM.unmountComponentAtNode()
We use this method when we need to remove a DOM node after it’s mounted, and clean up its event handlers and state.
We will continue on with our code again, this time we will unmount our root div
:
ReactDOM.unmountComponentAtNode(container)
For the container, we are passing in the root div, so when the user clicks on the button, it will unmount the app.
If you try to unmount the modal id
, you will see an error. This is because the modal is not mounted, so it will return false
:
// App.js const App = () => { ReactDOM.unmountComponentAtNode(document.getElementById("root")); return ( <button onClick={handleUnmount} className='button'> Unmount App </button> ) }
This code is enough to unmount the root.
ReactDOM.findDOMNode()
We know that we can render our DOM elements with the render
method. We can also get access to the underlying DOM node with the help of findDOMNode
method. According to the React documentation, this method is discouraged because it pierces the component abstraction.
NOTE:
findDOMNode
method has been deprecated in StrictMode.
Generally, if you need to refer to any DOM element, it’s advised to use the useRef
hook. In most cases, you can attach a ref to the DOM node and avoid using findDOMNode
at all.
Another key point is the Node element that you want to access must be mounted, which means it must be in the DOM. If it’s not mounted, findDOMNode
returns null. Once you get access to the mounted DOM node, you can use the familiar DOM API to inspect the node.
findDOMNode
takes one parameter, and it’s the component:
ReactDOM.findDOMNode(component)
We will continue with the code that we used on the createPortal
method. I created a new button and added the text Find The Modal Button and Change its Background Color
, and added an onClick
handler function. With this function, I accessed its className
with the document.querySelector
method and changed its background color to black:
const App = () => { const handleFindDOMNode = () => { const node = document.querySelector('.node'); ReactDOM.findDOMNode(node).style.backgroundColor = 'black'; }; return ( <button onClick={handleFindDOMNode} className='button'> Find The Modal Button and Change its Background Color </button> ) }
ReactDOM.hydrate()
and Server-Side Rendering (SSR)The hydrate
method will help us pre-render everything on the server side, then send the user the complete markup. It is used to add content to a container that was rendered by the ReactDOMServer.
This may sound like a gibberish right now, but the main takeaway is that we can render our React applications on the client or server side. Here’s a quick overview of the main differences between Client-Side Rendering (CSR) and Server-Side Rendering (SSR).
When we create and run our project with create-react-app
, it doesn’t show the contents of the page:
As you can see from the screenshot, we have only our divs and a reference to our JavaScript bundle, nothing else. So, this is actually a blank page. This means when we first load our page, the server makes a request to the HTML, CSS, and JavaScript. After the initial render, the server checks our bundled JavaScript (or React code, in our case) and paints the UI. This approach has some pros and cons.
Pros:
Cons:
With Server-Side Rendering, we don’t render an empty page anymore. With this approach, the server creates static HTML files which the browser renders.
This is how SSR works: when the user requests a website, the server renders the static version of the app, allowing users to see the website is loaded. The website is not interactive yet, so when the user interacts with the app, the server downloads the JavaScript and executes it.
The site becomes responsive by replacing the static content with the dynamic content. ReactDOM.hydrate()
function is actually called on the load event of these scripts and hooks the functionality with the rendered markup.
If you are curious, when we create our project with SSR, we can see the HTML and JavaScript code rendered at initial load:
Pros:
Cons:
ReactDOM.hydrate()
As you can imagine, for this to work, we need to create a server. We will be creating the server with Express, but first we need to do some cleanup.
To run a Node.js server using hydrate
, we need to remove all references to the window
and document
, because we will render our markup in the server, not in the browser.
Let’s go to the Modal.js
file and move the document.querySelector
inside the modal component:
// Modal.js import { createPortal } from 'react-dom'; let modalRoot; const Modal = ({ children }) => { modalRoot = modalRoot ? modalRoot : document.querySelector('#modal'); return createPortal(children, modalRoot); }; export default Modal;
Next up, we need to change ReactDOM.render
to ReactDOM.hydrate
inside the src/index.js
file:
ReactDOM.hydrate( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
Now, we can create our server. Create a new folder called server
and create a file inside this folder named server.js
. Install Express with npm install express
:
// server/server.js import express from 'express'; import fs from 'fs'; import path from 'path'; import React from 'react'; import { renderToString } from 'react-dom/server'; import App from '../src/App'; const app = express(); app.use('^/$', (req, res, next) => { fs.readFile(path.resolve('./build/index.html'), 'utf-8', (err, data) => { if (err) { console.error(err); return res.status(500).send('Error'); } return res.send( data.replace( '<div id="root"></div>', `<div id="root">${renderToString(<App />)}</div>` ) ); }); }); app.use(express.static(path.resolve(__dirname, '..', 'build'))); app.listen(3000, () => { console.log('Listening on port 3000'); });
Here we have required Express, the fs
(file system) module, the path, React, ReactDOMServer.renderToString
, and App
from our src folder.
The ReactDOMServer.renderToString
returns the static HTML version of our app.
Next, run the build command npm run build
to create a build
folder. Configure Babel, then install npm i @babel/preset-env @babel/preset-react @babel/register ignore styles
. Finally, create a new file named server/index.js
:
// server/index.js require('ignore-styles'); require('@babel/register')({ ignore: [/(node_modules)/], presets: ['@babel/preset-env', '@babel/preset-react'], }); require('./server');
Add a script for SSR in package.json
: "ssr": "node server/index.js"
. Run the server with npm run ssr
.
Remember, if you make changes in your app, first run npm run build
, and then npm run ssr
.
hydrate
with React 18In React 18, a new Suspense-based SSR architecture is introduced. The hydrate
method will also be replaced by hydrateRoot
.
We have covered a lot about ReactDOM. To summarize, here are the key takeaways we learned throughout this post:
render
method to render our UI components to the browser, the most-often used ReactDOM methodcreateRoot
method instead of the render
method with React 18createPortal
methodunmountComponentAtNode
methodfindDOMNode
method, but the best way is to use ref
insteadhydrate
method, just be prepared for React 18 and Suspense-based SSR architecture
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.