DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 7: React DOM and Virtual DOM

React DOM and Virtual DOM

Introduction to DOM and Browser Rendering

Before diving into our topic React DOM and Virtual DOM, it’s important to understand what the DOM is and how browsers render web pages.

What is the DOM?

The Document Object Model (DOM) is a programming interface for web documents. It represents the page as a tree of nodes that browsers use to render the page. Each HTML element becomes a node in this tree.

For example, this HTML:

<!DOCTYPE html>
<html>
<head>
  <title>My Page</title>
</head>
<body>
  <div id="container">
    <h1>Hello World</h1>
    <p>This is a paragraph.</p>
  </div>
</body>
</html>

Creates this DOM tree:

- html
  - head
    - title: "My Page"
  - body
    - div#container
      - h1: "Hello World"
      - p: "This is a paragraph."

How Browsers Render Pages

When a browser loads a page, it follows these steps:

  1. Parse HTML to construct the DOM tree
  2. Parse CSS to construct the CSSOM (CSS Object Model) tree
  3. Combine DOM and CSSOM to create a render tree
  4. Layout (or reflow) to compute the exact position and size of each element
  5. Paint the pixels to the screen

This process is computationally expensive, especially the layout and paint steps. When JavaScript changes the DOM, the browser often needs to recalculate styles, perform layout, and repaint, which can lead to performance issues.

The Problem: DOM Manipulation is Expensive

Direct DOM manipulation can be slow for several reasons:

  1. Layout thrashing: Repeatedly reading and writing to the DOM forces the browser to recalculate layouts multiple times
  2. Inefficient updates: Updating more elements than necessary
  3. Excessive repaints: Causing the browser to redraw parts of the screen unnecessarily

Before React, developers used libraries like jQuery to make DOM manipulation easier, but these tools didn’t solve the underlying performance issues.

React’s Solution: The Virtual DOM

React addresses these performance challenges with its Virtual DOM implementation.

What is Virtual DOM?

The Virtual DOM is a lightweight JavaScript representation of the real DOM. It’s essentially a tree of JavaScript objects that mimics the structure of the DOM.

// Example of a Virtual DOM representation
const virtualNode = {
  type: 'div',
  props: {
    id: 'container',
    children: [
      {
        type: 'h1',
        props: {
          children: 'Hello World'
        }
      },
      {
        type: 'p',
        props: {
          children: 'This is a paragraph.'
        }
      }
    ]
  }
};

How the Virtual DOM Works

React’s Virtual DOM process follows these steps:

  1. Initial render: React creates a virtual DOM tree from your components
  2. Conversion: React converts the virtual DOM to real DOM elements
  3. When state changes: React creates a new virtual DOM tree
  4. Diffing: React compares the new virtual DOM with the previous one using a diffing algorithm
  5. Reconciliation: React identifies the minimal set of changes needed to update the real DOM
  6. Batched updates: React applies all the necessary changes to the real DOM in a single batch

Show Image

React Fiber: The Reconciliation Engine

In React 16, the team introduced a new reconciliation engine called React Fiber.

What is React Fiber?

React Fiber is a complete rewrite of React’s core algorithm. Its main features include:

  1. Incremental rendering: Breaking rendering work into chunks that can be paused, resumed, and prioritized
  2. Error boundaries: Better error handling in components
  3. Improved support for animation, layout, and gestures
  4. Foundation for async rendering

How Fiber Works

Fiber works by creating a linked list of nodes (fibers) that can be processed incrementally. This allows React to:

  1. Pause work and come back to it later
  2. Assign priority to different types of updates
  3. Reuse previously completed work
  4. Abort work if it’s no longer needed

ReactDOM: The Bridge to the Browser

The react-dom package serves as the bridge between React’s Virtual DOM and the browser’s real DOM.

Key ReactDOM Functions

ReactDOM.render()

This is the main function for rendering React elements to the DOM:

import React from 'react';
import ReactDOM from 'react-dom';

const element = <h1>Hello, world!</h1>;
ReactDOM.render(element, document.getElementById('root'));

In React 18+, this has been replaced with ReactDOM.createRoot():

import React from 'react';
import { createRoot } from 'react-dom/client';

const element = <h1>Hello, world!</h1>;
const root = createRoot(document.getElementById('root'));
root.render(element);

ReactDOM.hydrate()

Used for server-side rendering to “hydrate” a container whose HTML contents were rendered by ReactDOMServer:

import React from 'react';
import ReactDOM from 'react-dom';

const element = <h1>Hello, world!</h1>;
ReactDOM.hydrate(element, document.getElementById('root'));

In React 18+, use hydrateRoot():

import React from 'react';
import { hydrateRoot } from 'react-dom/client';

const element = <h1>Hello, world!</h1>;
hydrateRoot(document.getElementById('root'), element);

ReactDOM.findDOMNode() (Legacy)

Returns the browser DOM element for a component instance:

import React from 'react';
import ReactDOM from 'react-dom';

class MyComponent extends React.Component {
  componentDidMount() {
    const domNode = ReactDOM.findDOMNode(this);
    // Do something with the DOM node
  }
 
  render() {
    return <div>My Component</div>;
  }
}

Note: findDOMNode() is deprecated in strict mode. Instead, use refs.

ReactDOM.createPortal()

Renders children into a DOM node that exists outside the DOM hierarchy of the parent component:

import React from 'react';
import ReactDOM from 'react-dom';

function Modal({ children }) {
  return ReactDOM.createPortal(
    children,
    document.getElementById('modal-root')
  );
}

function App() {
  return (
    <div>
      <h1>App Content</h1>
      <Modal>
        <h2>Modal Content</h2>
        <p>This appears in the modal container, not in the App div.</p>
      </Modal>
    </div>
  );
}

The Reconciliation Process in Detail

1. Component Tree to Virtual DOM

When you write a React component:

function App() {
  return (
    <div className="app">
      <Header title="My App" />
      <Content />
    </div>
  );
}

React creates a Virtual DOM representation:

{
  type: 'div',
  props: {
    className: 'app',
    children: [
      {
        type: Header,
        props: { title: 'My App' }
      },
      {
        type: Content,
        props: {}
      }
    ]
  }
}

2. Diffing Algorithm

When state or props change, React creates a new Virtual DOM tree and compares it with the previous one using a diffing algorithm.

Key Assumptions in React’s Diffing Algorithm

  1. Different component types produce different trees: If a div changes to a span, React rebuilds the entire subtree rather than trying to match children.
  2. Elements with stable keys maintain identity across renders: The key prop helps React identify which items have changed, been added, or been removed.

Diffing Process

  1. Root Elements: If the root elements are different types, React tears down the old tree and builds the new one from scratch.
  2. Same Type DOM Elements: React looks at the attributes of both and only updates the changed attributes.
// Before
<div className="before" title="tooltip">Content</div>

// After
<div className="after" title="tooltip">Content</div>// React only updates the className
  1. Same Type Component Elements: The component instance stays the same, only the props are updated, and componentDidUpdate() or appropriate hooks are called.
  2. Recursion on Children: By default, React recursively compares children in order.

// Before
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// After
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>
// React adds the third list item
  1. Keys: When children have keys, React uses the key to match children in the original tree with children in the subsequent tree.
// Before
<ul>
  <li key="1">first</li>
  <li key="2">second</li>
</ul>

// After
<ul>
  <li key="2">second</li>
  <li key="1">first</li>
</ul>

// React moves the elements instead of recreating them

3. Batch Updates

React batches state updates and DOM manipulations to minimize browser reflows and repaints. This is especially important for complex UI updates.

Keys in React: The Special Prop

As shown in the diffing algorithm, keys play a crucial role in efficient list rendering.

The Importance of Keys

Keys help React identify which items have changed, been added, or been removed in a list. They should be stable, predictable, and unique among siblings.

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Common Key Mistakes

Using Index as Key (Usually Bad)

// Not recommended unless the list is static and never reordered
{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

Using the array index as a key can cause issues if the list order changes or items are added/removed in the middle of the list.

Not Using Keys at All

// Missing keys warning
{items.map(item => (
  <ListItem item={item} />
))}

React will use indices by default and issue a warning.

When to Use Index as Key

Sometimes using the index as a key is acceptable:

  1. The list is static and will not change
  2. The items in the list have no stable IDs
  3. The list will never be reordered or filtered

Measuring Performance with React DevTools

React DevTools provides tools for measuring the performance impact of your components and the Virtual DOM reconciliation process.

Profiler Tab

React DevTools includes a Profiler tab that lets you record and analyze component rendering performance.

  1. Open React DevTools in your browser
  2. Switch to the Profiler tab
  3. Click the Record button
  4. Interact with your app
  5. Stop recording and analyze the results

The Profiler shows:

  • Which components rendered and why
  • How long each component took to render
  • The “commit” timeline for rendering updates

Using React.memo for Optimization

React.memo is a higher-order component that memoizes your component, preventing unnecessary re-renders if props haven’t changed:

const MemoizedComponent = React.memo(function MyComponent(props) {
  // Your component code
});

You can also provide a custom comparison function:

const MemoizedComponent = React.memo(
  function MyComponent(props) {
    // Your component code
  },
  (prevProps, nextProps) => {
    // Return true if passing nextProps to render would return
    // the same result as passing prevProps to render
    return prevProps.value === nextProps.value;
  }
);

React.StrictMode and DOM Operations

React.StrictMode is a tool for highlighting potential problems in your application during development:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

StrictMode:

  1. Identifies unsafe lifecycle methods
  2. Warns about legacy string ref API usage
  3. Warns about deprecated findDOMNode usage
  4. Detects unexpected side effects
  5. Ensures reusable state

It helps you spot potential issues related to DOM operations and the rendering process.

Practical Example: List with Optimized Rendering

Let’s build a todo list that demonstrates efficient DOM updates using React’s Virtual DOM and keys:

import React, { useState } from 'react';

// Individual Todo item component
const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete }) {
  console.log(`Rendering TodoItem: ${todo.id}`);
 
  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span className="todo-text">{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

// Todo List Component
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: true },
    { id: 2, text: 'Build a project', completed: false },
    { id: 3, text: 'Deploy to production', completed: false }
  ]);
 
  const [newTodoText, setNewTodoText] = useState('');
  const [nextId, setNextId] = useState(4);
 
  const handleAddTodo = () => {
    if (!newTodoText.trim()) return;
   
    setTodos([
      ...todos,
      {
        id: nextId,
        text: newTodoText,
        completed: false
      }
    ]);
   
    setNextId(nextId + 1);
    setNewTodoText('');
  };
 
  const handleToggleTodo = (id) => {
    setTodos(
      todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };
 
  const handleDeleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
 
  return (
    <div className="todo-app">
      <h1>Todo List</h1>
     
      <div className="add-todo">
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="Add a new todo"
        />
        <button onClick={handleAddTodo}>Add</button>
      </div>
     
      <ul className="todo-list">
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={handleToggleTodo}
            onDelete={handleDeleteTodo}
          />
        ))}
      </ul>
     
      <div className="todo-stats">
        <p>{todos.filter(todo => todo.completed).length} completed / {todos.length} total</p>
      </div>
    </div>
  );
}

export default TodoList;

Key Optimization Points in the Example

  1. Proper Key Usage: Each TodoItem has a stable, unique id as its key
  2. React.memo: The TodoItem component uses React.memo to prevent unnecessary re-renders
  3. State Updates: State updates are done immutably, which helps React identify changes
  4. Component Composition: By breaking the UI into components, React can update only what’s necessary

Summary

In this chapter, we’ve covered:

  • The Document Object Model (DOM) and how browsers render web pages
  • The performance challenges of direct DOM manipulation
  • React’s Virtual DOM as a solution to these challenges
  • The reconciliation process and diffing algorithm
  • React Fiber architecture
  • Key ReactDOM functions for working with the browser
  • The importance of keys in efficient list rendering
  • Performance measurement tools
  • Optimization techniques like React.memo
  • A practical example of optimized list rendering

Understanding the Virtual DOM is crucial for optimizing React applications. By leveraging React’s efficient rendering strategy and following best practices, you can build performant applications even with complex UIs.

Additional Resources

Scroll to Top