Hulya Karakaya A frontend developer interested in open source and building amazing websites. I believe in building through collaboration and contribution.

Managing DOM components with ReactDOM

10 min read 2821

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

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.

Virtual DOM (VDOM) vs 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.

We made a custom demo for .
No really. Click here to check it out.

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>

Diagram of DOM tree structure

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'));

Goodbye 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 [email protected] [email protected]

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;

Gif of a modal popping up after clicking a button

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.

Gif of modal appearing at button click with "unmount app" button

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>
  )
}

Same gif as before, but with an additional button that reads "find the modal button and make it black"

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).

Client-Side Rendering (CSR)

When we create and run our project with create-react-app, it doesn’t show the contents of the page:

Screenshot of the code for the example app

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:

  • Quick
  • Static deployment
  • Supports Single Page Applications (SPA)

Cons:

  • Renders a blank page at initial load
  • Bundle size may be large
  • Not good for SEO

Server- Side Rendering (SSR)

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:

Screenshot of code for the example app with SSR

Pros:

  • Better performance
  • Great for SEO to help us create easily indexable and crawlable websites
  • Fast interactivity
  • Speeds uploading time by running React on the server before serving the request to the user.

Cons:

  • Creates lots server requests
  • If you have lots of interactive elements on your site, it can slow down the rendering

Demo of 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.

Changes to hydrate with React 18

In React 18, a new Suspense-based SSR architecture is introduced. The hydrate method will also be replaced by hydrateRoot.

Conclusion

We have covered a lot about ReactDOM. To summarize, here are the key takeaways we learned throughout this post:

  • React uses a Virtual DOM, which helps us prevent unnecessary DOM repaints, and updates only what has changed in the UI
  • We use the render method to render our UI components to the browser, the most-often used ReactDOM method
  • We use the createRoot method instead of the render method with React 18
  • We can create modals and tooltips with the createPortal method
  • We can unmount a component with the unmountComponentAtNode method
  • We can get access to any DOM node with the findDOMNode method, but the best way is to use ref instead
  • We can use SSR in React with the help of the hydrate method, just be prepared for React 18 and Suspense-based SSR architecture
  • SSR helps us pre-render everything on the server side for better SEO optimization

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Hulya Karakaya A frontend developer interested in open source and building amazing websites. I believe in building through collaboration and contribution.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply