Rendering elements and the Virtual DOM

Acadestine

Rendering Elements and the Virtual DOM in React

Rendering is the process by which React converts your components into the actual user interface users see in the browser. At the heart of this process is the Virtual DOM — a lightweight JavaScript representation of the real DOM that enables React to update the UI efficiently. Understanding how rendering works and how the Virtual DOM optimizes updates is essential for writing performant React applications.


Rendering is the process of converting your React components (JSX) into DOM elements displayed in the browser.

The Rendering Pipeline

JSX → React Element (JS Object) → Virtual DOM → Real DOM → Browser Paint

Why React's Rendering is Different

Traditional DOM React
Manually update DOM Describe UI declaratively
Directly manipulate nodes React handles updates
Re-render entire sections Only update what changed
Imperative Declarative

Key Concepts

Concept Description
React Element A plain JavaScript object describing what to render
Virtual DOM Lightweight in-memory representation of the real DOM
Reconciliation Algorithm React uses to compare Virtual DOM trees
Diffing Process of finding differences between two trees
Commit Applying changes to the real DOM
Render React calculating what the UI should look like

Key Insight: In React, "rendering" does not mean updating the browser DOM. It means React is calculating what the UI should look like. The actual DOM update happens during the "commit" phase.


A React Element is the smallest building block of a React application. It is a plain JavaScript object that describes what you want to see on the screen.

Creating a React Element

// JSX syntax (recommended)
const element = <h1>Hello, world!</h1>;

// Equivalent JavaScript (what JSX compiles to)
const element = React.createElement('h1', null, 'Hello, world!');

// The resulting React Element (simplified object)
const element = {
    type: 'h1',
    props: {
        children: 'Hello, world!'
    }
};

React Element Characteristics

Characteristic Description
Immutable Once created, a React element cannot be changed.
Plain Object A React element is a plain JavaScript object that describes what should be rendered.
Lightweight Elements are lightweight and inexpensive to create and compare.
Description An element describes what the UI should look like, not the actual DOM element itself.

Elements vs Components

Element Component
A plain JavaScript object A function or class
Describes what to render Returns React elements
Immutable Can contain state and other logic
Example: <h1>Hello</h1> Example: function Greeting() { return <h1>Hello</h1>; }

Example

const element = <h1>Hello</h1>;

The JSX above creates a React element. It is a description of what React should render.

A component can create and return elements:

function Greeting() {
  return <h1>Hello</h1>;
}

Here, Greeting is a component, while <h1>Hello</h1> is a React element.

Elements Are Immutable

Once a React element has been created, you cannot change its properties.

For example:

const element = <h1>Hello</h1>;

You cannot modify element to make it say "Hello World".

Instead, you create a new element:

const element = <h1>Hello World</h1>;

React then compares the new element with the previous one and determines what needs to change in the DOM.

// Once created, an element cannot be modified
const element = <h1>Hello, world!</h1>;

// This creates a NEW element, it does not modify the existing one
const newElement = <h1>Hello, React!</h1>;

// The only way to update the UI is to create a new element
// and pass it to ReactDOM.render()

Best Practice: Don't try to modify React elements directly. Instead, create new elements based on state changes.


ReactDOM is the library that bridges React elements and the actual browser DOM.

Modern ReactDOM (React 18+)

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

// Create a root
const root = ReactDOM.createRoot(document.getElementById('root'));

// Render an element
const element = <h1>Hello, world!</h1>;
root.render(element);

Legacy ReactDOM (React 17 and earlier)
import React from 'react';
import ReactDOM from 'react-dom';

// Legacy rendering method (deprecated in React 18)
ReactDOM.render(
<h1>Hello, world!</h1>, document.getElementById('root') ); 

Key Concepts Concept Description Root The DOM node where React manages the UI createRoot Creates a root for React 18+ concurrent features render Renders a React element into the root Unmount Removing React from a DOM node Unmounting

// Unmount the root (removes React from the DOM node)
root.unmount();

Updating the Rendered Element

// React elements are immutable
// To update the UI, create a new element and render it

function tick() {
    const element = (
        <div>
            <h1>Hello, world!</h1>
            <h2>It is {new Date().toLocaleTimeString()}.</h2>
        </div>
    );
    root.render(element);
}

setInterval(tick, 1000);

Important: In React 18, ReactDOM.render is deprecated. Use ReactDOM.createRoot instead for concurrent features.

The Virtual DOM is a lightweight, in-memory representation of the real DOM. It allows React to determine what changed without directly manipulating the browser DOM for every update. Why the Virtual DOM? Problem with Real DOM How Virtual DOM Helps Expensive to update Compare in memory first Full re-render required Update only what changed Browser repaints costly Batch updates efficiently Manual tracking needed Automatic diffing How the Virtual DOM Works

Initial Render – React creates a Virtual DOM tree from your elements.

State/Props Change – A new Virtual DOM tree is created.

Diffing – React compares the new tree with the previous one.

Identify Changes – React determines the minimal set of updates.

Batch Update – React applies the changes to the real DOM.

Virtual DOM Diagram

┌─────────────────────────────────────────────────────────┐ │ Initial Render │ │ │ │ React Elements → Virtual DOM Tree → Real DOM │ │ │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ State Change │ │ │ │ New React Elements → New Virtual DOM Tree │ │ │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Diffing │ │ │ │ Compare Old Virtual DOM with New Virtual DOM │ │ │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Commit │ │ │ │ Apply only the differences to the Real DOM │ │ │ └─────────────────────────────────────────────────────────┘

Virtual DOM vs Real DOM Feature Real DOM Virtual DOM Speed Slow (browser repaints) Fast (in-memory) Manipulation Direct DOM API React manages Memory Large Small Updates Manual Automatic (diffing) Representation Browser nodes JavaScript objects

Key Insight: The Virtual DOM is not faster than the real DOM at updating. It's faster at determining what needs to be updated.

Reconciliation is the algorithm React uses to compare two Virtual DOM trees and determine what changes need to be applied to the real DOM. The Diffing Algorithm

React uses a heuristic O(n) algorithm to compare trees efficiently. Key Assumptions

Different Element Types – Elements of different types produce different trees.

Keys – Developers can hint at which child elements are stable using keys.

Diffing Rules Scenario Behavior Different element types Tear down old tree, build new tree Same element type Update attributes/props Same component Update props, keep state Different component Unmount old, mount new Diffing Example

// Before
<div> <h1>Hello</h1> <p>World</p> </div>

// After
<div> <h1>Hello</h1> <p>React</p> </div>

// React compares:
// - div: same type → keep
// - h1: same type, same content → no change
// - p: same type, different content → update text
// Result: Only update the <p> element's text

Element Type Change

// Before
<div>
    <Counter />
</div>

// After
<span>
    <Counter />
</span>

// React behavior:
// - div vs span: different type → destroy entire tree
// - Rebuild from scratch (including Counter state)

Keys and Reconciliation

Keys help React identify which items have changed, been added, or removed.

// Without keys (inefficient)
<ul> <li>Apple</li> <li>Banana</li> <li>Orange</li> </ul>

// With keys (efficient)
<ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> 

Why Keys Matter Without Keys With Keys React re-renders all items React updates only changed items State may be lost State is preserved Performance issues Performance optimized Bugs with inputs Stable behavior Reconciliation Phases Phase Description Render Phase React calculates what needs to change (can be interrupted) Commit Phase React applies changes to the real DOM (synchronous)

Important: Never use array indices as keys if the list can be reordered. Use stable, unique identifiers instead.

React Fiber is the reconciliation engine introduced in React 16. It enables incremental rendering and prioritization of updates. Fiber Features Feature Description Incremental Rendering Split rendering work into chunks Pausable Work Pause and resume rendering Priority-Based Prioritize urgent updates Concurrent Mode Render multiple versions simultaneously Fiber Tree

The Fiber tree is a linked-list representation of the Virtual DOM tree. FiberNode { type: 'div', child: FiberNode, sibling: FiberNode, return: FiberNode, alternate: FiberNode, ... }

Fiber Phases Phase Description Render Phase Build the Fiber tree, calculate changes (interruptible) Commit Phase Apply changes to the DOM (not interruptible) Concurrent Features (React 18)

import { useTransition, useDeferredValue } from 'react';

// useTransition: Mark updates as non-urgent
function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
// Urgent: Update input
setQuery(e.target.value);

// Non-urgent: Update search results
startTransition(() => {
// Update results
});
};
}

// useDeferredValue: Defer expensive updates
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// Use deferredQuery for expensive computation
}

Best Practice: Use React 18's concurrent features (useTransition, useDeferredValue) to keep the UI responsive.


Common Optimization Techniques

Technique Description
React.memo Prevent re-renders if props haven't changed
useMemo Memoize expensive calculations
useCallback Memoize functions
Keys Proper keys for list items
Code Splitting Lazy load components
Virtualization Render only visible items

React.memo Example

import React from 'react';

// Prevents re-render if props haven't changed
const MyComponent = React.memo(function MyComponent({ name }) {
    console.log('Rendering MyComponent');
    return <div>Hello, {name}</div>;
});

// Only re-renders when name prop changes

useMemo and useCallback

``` js import { useMemo, useCallback } from 'react';

function ExpensiveComponent({ items, onItemClick }) { // Memoize expensive calculation const sortedItems = useMemo(() => { return items.sort((a, b) => a.name.localeCompare(b.name)); }, [items]);

// Memoize function const handleClick = useCallback((id) => { onItemClick(id); }, [onItemClick]);

return (

    {sortedItems.map(item => (
  • handleClick(item.id)}> {item.name}
  • ))}

); }

``` Virtualization Example

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
return (
<FixedSizeList
height={400}
width={300}
itemCount={items.length}
itemSize={35}

{({ index, style }) => (
<div style={style}> {items[index].name} </div> )} </FixedSizeList> ); } 

Best Practice: Only optimize when you have a performance problem. Premature optimization can make code harder to maintain.

How Developers Should Think About Rendering

Understand the Render Cycle – Know when and why components re-render.

Avoid Unnecessary Renders – Use React.memo, useMemo, useCallback.

Use Keys Correctly – Always use stable, unique keys.

Profile Performance – Use React DevTools Profiler.

Leverage Concurrent Features – Use useTransition and useDeferredValue.

Developer Checklist

□

Are you using keys correctly in lists?
□

Are you avoiding unnecessary re-renders?
□

Are you using React.memo for expensive components?
□

Are you using useMemo for expensive calculations?
□

Are you using useCallback for stable function references?
□

Have you profiled your app with React DevTools?

Example: Debugging Re-renders

import { useEffect, useRef } from 'react';

function useRenderCount() {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount.current;
}

function MyComponent({ name }) {
const renders = useRenderCount();
console.log(MyComponent rendered ${renders} times);
return <div>Hello, {name}</div>;
}

React DevTools Profiler

The React DevTools Profiler helps identify performance bottlenecks:

  1. Open React DevTools.
  2. Go to the Profiler tab.
  3. Click Record.
  4. Interact with your app.
  5. Stop recording and analyze.

  • Rendering – React calculates what the UI should look like.
  • React Elements – Immutable JavaScript objects describing the UI.
  • ReactDOM – Bridges React elements and the real DOM.
  • Virtual DOM – In-memory representation of the real DOM.
  • Reconciliation – Algorithm for comparing Virtual DOM trees.
  • Diffing – Finding minimal changes between trees.
  • Keys – Help React identify stable elements.
  • React Fiber – Reconciliation engine with incremental rendering.
  • Optimization – React.memo, useMemo, useCallback, virtualization.
Key Takeaway

"The Virtual DOM is React's secret weapon for efficient UI updates. Understanding how rendering, reconciliation, and diffing work empowers you to write faster, more predictable React applications."

Happy coding!

Previous Next Lesson