Introduction
Hi, Gleb Kotovsky is here!
Today I wanna talk about Virtual DOM, specifically - React Virtual DOM
So, the virtual DOM (Virtual Document Object Model) is a cool programming idea that keeps a “virtual” version of a user interface in memory. This version syncs up with the browser’s DOM (Document Object Model) using a library.
You’ll find the virtual DOM is a big part of many JavaScript front-end frameworks, and it’s one of the reasons they’re so efficient. In this article, we’re going to dive into how the virtual DOM works in React and why it’s important for the library.
What is the DOM?
When a webpage loads in a browser, it typically receives an HTML document from the server. The browser then builds a logical, tree-like structure from this HTML to render the requested page for the user. This structure is known as the DOM.
The Document Object Model (DOM) represents a logical tree that describes a document. Each branch of the tree ends in a node
, which contains an object
. Because the browser parses the document into this tree structure, there is a need for methods that allow for programmatic access to the tree, enabling modifications to the document’s structure, style, or content. This necessity led to the development of the DOM API, which offers these methods for manipulating the nodes representing the elements in the tree.
React’s Virtual DOM Implementation
To optimize re-rendering in websites and applications, many JavaScript frameworks offer different strategies. However, React employs the concept of the virtual DOM.
The virtual DOM in React represents the user interface as a “virtual” tree structure, where each element is a node containing an object. This representation is maintained in memory and synchronized with the browser’s DOM through React’s React DOM library.
When React and many other famous frameworks uses Virtual DOM, Svelte meanwhile has no Virtual DOM. Svelte works directly with the DOM in the browser and modifies it as needed.
Here’s a simple example to illustrate the Virtual DOM in a React component:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
In this example:
-
The component renders a counter and a button.
-
When the button is clicked, the state is updated, prompting React to create a new Virtual DOM tree.
-
The diffing algorithm checks what has changed (only the count) and updates the real DOM accordingly.
After the component is first rendered and the state is count: 0
, the actual DOM will look like this:
<div>
<h1>Counter</h1>
<p>Count: 0</p>
<button>Increment</button>
</div>
How the Virtual DOM Works:
Here’s a simple example to illustrate the Virtual DOM in a React component, starting with the component definition:
1. Component Definition
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
2. Initial Render Process
2.1 Component Initialization
When the component is first rendered, React calls the Counter
function.
2.2 State Initialization
useState(0)
initializes the component’s state to 0
.
2.3 Creating the Virtual DOM
React generates a Virtual DOM tree using the component’s returned JSX structure. This tree is a lightweight representation of the UI.
For the initial render, the Virtual DOM might look like this:
{
"type": "div",
"props": {
"children": [
{ "type": "h1", "props": { "children": "Counter" } },
{ "type": "p", "props": { "children": "Count: 0" } },
{ "type": "button", "props": { "children": "Increment" } }
]
}
}
2.4 Updating the Real DOM
React then takes this Virtual DOM and calculates what changes need to be made to the actual DOM. In this case, it creates the following HTML:
<div>
<h1>Counter</h1>
<p>Count: 0</p>
<button>Increment</button>
</div>
3. User Interaction
When a user clicks the “Increment” button, the following steps occur:
3.1 Event Handling
The button’s onClick
event triggers the increment
function, calling setCount(count + 1)
.
3.2 State Update
The component’s state is updated, which causes React to know that it needs to re-render the component with the new state.
4. Re-render Process
4.1 Component Re-invocation
React calls the Counter
function again due to the state change.
4.2 New Virtual DOM Creation
A new Virtual DOM tree is created reflecting the updated state:
{
"type": "div",
"props": {
"children": [
{ "type": "h1", "props": { "children": "Counter" } },
{ "type": "p", "props": { "children": "Count: 1" } },
{ "type": "button", "props": { "children": "Increment" } }
]
}
}
4.3 Diffing the Virtual DOM
React compares the new Virtual DOM with the previous Virtual DOM. It identifies what has changed—in this case, the text in the <p>
tag has changed from “Count: 0” to “Count: 1”.
4.4 Reconciliation
Only the parts of the real DOM that have changed are updated. In this case, React updates the real DOM to reflect the new count:
<div>
<h1>Counter</h1>
<p>Count: 1</p> <!-- Updated content -->
<button>Increment</button>
</div>
5. Performance Optimization
5.1 Batching Updates
If multiple state updates occur in rapid succession (e.g., multiple button clicks), React may batch these updates together for efficiency, minimizing the number of re-renders and DOM updates.
Common Problems with React Virtual DOM and How to Avoid Them
- Performance Bottlenecks
- Issue: Excessive re-renders can occur even with the Virtual DOM.
- Solution: Use
React.memo
to memoize functional components.
const MyComponent = React.memo(({ value }) => {
console.log('Rendered: ', value);
return <div>{value}</div>;
});
Legacy: Use
shouldComponentUpdate
in class components:
class MyClassComponent extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.value !== this.props.value;
}
render() {
return <div>{this.props.value}</div>;
}
}
- Inefficient Key Management
- Issue: Improper handling of keys in lists can lead to bugs.
- Solution: Use unique and stable keys, not array indices.
const items = ['Apple', 'Banana', 'Cherry'];
return (
<ul>
{items.map(item => (
<li key={item}>{item}</li> // Prefer unique values as keys
))}
</ul>
);
- Overusing State and Updates
- Issue: Too many state updates lead to performance issues.
- Solution: Combine related states
const [state, setState] = useState({
name: '',
age: 0,
});
const updateAge = (newAge) => {
setState(prevState => ({ ...prevState, age: newAge }));
};
- Using Inline Functions
- Issue: Inline functions create new instances on every render.
- Solution: Use
useCallback
to memoize functions.
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // Only recreate the function if dependencies change
- Deep Component Trees
- Issue: Deeply nested components trigger multiple re-renders.
- Solution: Use context.
const CountContext = React.createContext();
const ParentComponent = () => {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<ChildComponent />
</CountContext.Provider>
);
};
const ChildComponent = () => {
const { count, setCount } = useContext(CountContext);
return <div onClick={() => setCount(count + 1)}>Count: {count}</div>;
};
- Excessive Re-renders Due to Parent Component Updates
- Issue: Child components re-render when parents update.
- Solution: Memoize child components.
const ChildComponent = React.memo(({ count }) => {
return <div>Count: {count}</div>;
});
- Inefficient Rendering of Expensive Components
- Issue: Expensive components can slow down the app.
- Solution: Use
React.lazy
andReact.Suspense
.
const LazyComponent = React.lazy(() => import('./LazyComponent'));
const App = () => (
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
);
- Managing Side Effects
- Issue: Side effects can cause bugs if not managed properly.
- Solution: Use
useEffect
with proper dependencies.
useEffect(() => {
const timer = setTimeout(() => {
console.log('Time elapsed');
}, 1000);
return () => clearTimeout(timer); // Cleanup on unmount or if dependencies change
}, [dependencies]); // Replace with actual dependency
- Confusion Between State and Props
- Issue: Misunderstanding when to use state vs. props.
- Solution: Use props for externally managed data and state for local data.
const ParentComponent = () => {
const [name, setName] = useState('John');
return <ChildComponent name={name} setName={setName} />;
};
const ChildComponent = ({ name, setName }) => (
<div>
<p>{name}</p>
<button onClick={() => setName('Jane')}>Change Name</button>
</div>
);
- Neglecting Accessibility
- Issue: Accessibility concerns can be ignored.
- Solution: Use semantic HTML and accessibility tools.
const AccessibleButton = () => (
<button aria-label="Increment" onClick={increment}>
Increment
</button>
);
Conclusion
To wrap things up, React’s Virtual DOM is a fantastic feature that really boosts the performance of your web applications. By creating a lightweight version of the actual DOM, React can make updates more efficiently, avoiding the slowdowns that come with direct DOM manipulation.
That said, it’s important to watch out for common issues like excessive re-renders, poor key management in lists, and mixing up state and props. By keeping some best practices in mind—like using memoization, deploying context for handling state, and managing side effects wisely—you can get the most out of React and keep your apps running smoothly.
Happy hacking!