Virtual DOM (VDOM aka VNode) is magical ✨ but is also complex and hard to understand😱. React, Preact and similar JS libraries use them in their core. Unfortunately I couldn’t find any good article or doc that explains it in a detailed-yet-simple-to-understand fashion. So I thought of writing one myself.
Note: This is a LONG post. I’ve added tons of pictures to make it simple but it also makes the post appear even longer.
I’m using Preact’s code and VDOM as it is small and you can look at it yourself with ease in the future. But I think most of the concepts applies to React as well.
My hope is that once you read this, you’ll be able to better understand and hopefully contribute to libraries like React and Preact.
In this blog, I’ll take a simple example and go over various scenarios to give you an idea as to how they actually work. Specifically, I’ll go over:
- Babel and JSX
- Creating VNode — A single virtual DOM element
- Dealing with components and sub-components
- Initial rendering and creating a DOM element
- Re-rendering
- Removing DOM element.
- Replacing a DOM element.
The app:
The app a simple filterable Search app that contains two components “FilteredList” and “List”. The List renders a list of items (default: “California” and “New York”). The app has a search field that filters the list based on the characters in the field. Pretty straight forward.
Live app: http://codepen.io/rajaraodv/pen/BQxmjj
The Big Picture
At a high-level, we write components in JSX(html in JS), that gets converted to pure JS by CLI tool Babel. Then Preact’s “h” (hyperscript) function, converts it into VDOM tree (aka VNode). And finally Preact’s Virtual DOM algorithm, creates real DOM from the VDOM that creates our app.
Before we get into the weeds of the VDOM lifecycle, let’s understand JSX as it provides the starting point for the library.
1. Babel And JSX
In React, Preact like libraries, there is no HTML and instead everything is JavaScript. So we need to write even the HTML in JavaScript. But writing DOM in pure JS is a nightmare!😱
For our app we’ll have to write HTML like below:
Note: I’ll explain “h” soon
That’s where JSX comes in. JSX essentially allows us to write HTML in JavaScript! And also allows us to use JS within that by curly braces{}.
JSX helps us easily write our components like below:
Converting JSX tree to JavaScript
JSX is cool but it’s not a valid JS, but ultimately we need REAL DOM. JSX only helps in writing a representation of real DOM and otherwise it’s useless.
So we a way need to convert into a corresponding JSON object (VDOM, which is also a tree) so we can eventually use it as an input to create real DOM. We need a function to do that.
And that function is the “h” function in Preact. It’s the equivalent to “React.createElement” in React.
“h” stands for hyperscript — one of the first libs to create HTML in JS (VDOM)
But how to convert JSX into “h” function calls? And that’s where Babel comes in. Babel simply goes through each JSX node and converts them to “h” function calls.
Babel JSX (React Vs Preact)
By default, Babel converts JSX to React.createElement calls because it defaults to React.
But we can easily change the name of the function to anything we want (like “h” for Preact) by adding “Babel Pragma” like below:
Option 1:
//.babelrc
{ "plugins": [
["transform-react-jsx", { "pragma": "h" }]
]
}Option 2:
//Add the below comment as the 1st line in every JSX file
/** @jsx h */
Main Mount To real DOM
Not only the code in “render” methods of the components are converted to “h” functions, but also the starting mount!
And this is where the execution start and everything begins!
//Mount to real DOM
render(<FilteredList/>, document.getElementById(‘app’));//Converted to "h":
render(h(FilteredList), document.getElementById(‘app’));
The Output of “h” function
The “h” function takes the output of JSX and creates something called a “VNode” (React’s “createElement” creates ReactElement). A Preact’s “VNode” (or a React’s “Element”) is simply a JS object representation of a single DOM node with it’s properties and children.
It looks like this:
{
"nodeName": "",
"attributes": {},
"children": []
}
For example, VNode for our app’s Input looks like this:
{
"nodeName": "input",
"attributes": {
"type": "text",
"placeholder": "Search",
"onChange": ""
},
"children": []
}
Note: “h” function doesn’t create the entire tree! It simply creates JS object for a given node. But since the “render” method already has the DOM JSX in a tree fashion, the end result will be a VNode with children and grand children that looks like a tree.
Reference Code:
“h” : https://github.com/developit/preact/blob/master/src/h.js
VNode: https://github.com/developit/preact/blob/master/src/vnode.js
“render”: https://github.com/developit/preact/blob/master/src/render.js
“buildComponentFromVNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102
OK, let’s see how Virtual DOM works.
Virtual DOM Algorithm Flowchart For Preact
In the flowchart below shows how components (and child components) are created, updated and deleted by Preact. It also shows when lifecycle events like “componentWillMount” and so on are called.
Note: We’ll go over each section in a step-by-step manner so don’t worry if it looks complicated
Yes, it’s hard to understand all at once. So let’s go over various sections of the flowchart by going through various scenarios in a step-by-step manner.
Note: I’ll highlight sections of the lifecycle in “yellow” when discussing specific steps.
Scenario 1: Initial Creation Of The App
1.1 — Creating VNode (Virtual DOM) For A Given Component
The highlighted section shows the initial loop that creates VNode (Virtual DOM) tree for a given component. Note that this doesn’t create VNode for sub-components (that’s a different loop).
The picture below shows what happens when our app loads for the first time. The library ends up creating a VNode with children and attributes for the main FilteredList component.
Note: Along the way it also calls “componentWillMount” and “render” lifecycle methods (see the green blocks in the picture above).
At this point, we have a VNode that has a “div” parentNode that has an “input” and a “List” child nodes.
Reference code:
Most lifecycle events like: componentWillMount, render and so on: https://github.com/developit/preact/blob/master/src/vdom/component.js
1.2 — If Not A Component, create a REAL DOM
In this step, it’ll simply create real DOM for the parent node (div) and repeat process for child nodes (“input” and “List”).
At this point, we have just “div” as shown in the picture below:
Reference code:
document.createElement: https://github.com/developit/preact/blob/master/src/dom/recycler.js
1.3 — Repeat for all children
At this point, the loop is repeated for all children. In our app, it’ll be repeated for “input” and “List” items.
1.4 — Process Child And Append To Parent.
In this step, we’ll process leaf. Since “input” has a parent (“div”), we’ll append input as a child to div. Then the control stops and return to create “List” (which is the 2nd child of “div”).
At this point, our app looks like below:
Note: that after “input” is created, since it doesn’t have any children, it doesn’t immediately loop and create “List”! Instead it’ll first append “input” to the parent “div” and then goes back to process “List”
Reference code:
appendChild: https://github.com/developit/preact/blob/master/src/vdom/diff.js
1.5 Process child component(s)
The control goes back to step 1.1 and starts all 0ver again for “List” component. But since “List” is a component, it calls the render method of the “List” component to get new set of VNodes that look like below.
That loop completes for the List component and returns List’s VNode that looks like below:
Reference Code:
“buildComponentFromVNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102
1.6 Repeat steps 1.1 through 1.4 for all the Child Nodes
It’ll repeat the above steps again for each node. Once it reaches the leaf node, it appends it to the node’s parent and repeats the process.
The below picture shows how each node is added (hint: depth-first).
1.7 Finish processing
At this point, It’s done processing. It simply calls “componentDidMount” for all the components (starting from child components to parent components) and stops.
Important Note: Once everything is done, a reference to the real DOM is added to each of the component instances. This reference is used for remaining updates (create, update, delete) to compare and avoid recreating the same DOM nodes.
SCENARIO 2: Delete Leaf Node
Say we typed “cal” and hit enter. This will remove the 2nd list node, a leaf node (New York) while keeping all other parent nodes.
Let’s see how the flow looks for this scenario.
2.1 Create VNodes like before.
After initial rendering, every change in the future is an “update”. When it comes to creating VNodes, the update cycle works very similar to create cycle and creates VNodes all over again.
But since it’s an update (and not creation) of the component, it makes “componentWillReceiveProps”, “shouldComponentUpdate”, and “componentWillUpdate” calls to each component and sub-component.
In addition, update cycle, doesn’t recreate DOM elements if those elements are already there.
Reference Code
removeNode: https://github.com/developit/preact/blob/master/src/dom/index.js#L9
insertBefore: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L253
2.2 Use the reference real DOM node and avoid creating duplicate nodes
As mentioned earlier, each component has a reference to corresponding real DOM tree that was created during initial loading. The picture below shows how references look for our app at this point.
And when VNodes created, each VNode’s attributes are compared w/ the attributes of the REAL DOM at that node. If real DOM exists, the loop moves on to the next node.
Reference Code
innerDiffNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L185
2.3 Remove node if there are extra nodes in the REAL DOM
The picture below shows the difference in REAL DOM V/s VNode.
And since there is a difference, the “New York” node in REAL DOM is removed by the algorithm as shown in the workflow below. The algorithm also calls “componentDidUpdate” lifecycle event once everything is done.
SCENARIO 3 — Unmounting Entire Component
Use case: Let’s say if we typed blabla in the filter, since it doesn’t match “California” or “New York”, we won’t render the child component “List” at all. This means, we need to unmount the entire component.
Deleting a component is similar to Deleting a single node. Except, when we delete a node that has a reference to a component, then the framework calls “componentWillUnmount” and then recursively deletes all the DOM elements. After all the elements are removed from the real DOM, it calls “componentDidUnmount” method of the referenced component.
The picture below shows the reference to “List” component on the real DOM “ul”.
The below picture highlights the section in the flowchart to show how deleting/unmounting a component works.
Reference code
unmountComponent: https://github.com/developit/preact/blob/master/src/vdom/component.js#L250
Final Notes:
I hope that this post gave you enough idea as to how Virtual DOM works (at least in Preact).
Please note that while these scenarios covers major ones, I haven’t covered some of the optimizations in the code.
🙏🏼 Thank you!
If this was useful, please click the clap 👏 button down below a few times to show your support! ⬇⬇⬇ 🙏🏼
My Other Posts
ECMAScript 2015+
- Check out these useful ECMAScript 2015 (ES6) tips and tricks
- 5 JavaScript “Bad” Parts That Are Fixed In ES6
- Is “Class” In ES6 The New “Bad” Part?
Terminal Improvements
- How to Jazz Up Your Terminal — A Step By Step Guide With Pictures
- Jazz Up Your “ZSH” Terminal In Seven Steps — A Visual Guide
WWW
Virtual DOM
React Performance
Functional Programming
- JavaScript Is Turing Complete — Explained
- Functional Programming In JS — With Practical Examples (Part 1)
- Functional Programming In JS — With Practical Examples (Part 2)
- Why Redux Need Reducers To Be “Pure Functions”
WebPack
- Webpack — The Confusing Parts
- Webpack & Hot Module Replacement [HMR] (under-the-hood)
- Webpack’s HMR And React-Hot-Loader — The Missing Manual
Draft.js
React And Redux :
- Step by Step Guide To Building React Redux Apps
- A Guide For Building A React Redux CRUD App (3-page app)
- Using Middlewares In React Redux Apps
- Adding A Robust Form Validation To React Redux Apps
- Securing React Redux Apps With JWT Tokens
- Handling Transactional Emails In React Redux Apps
- The Anatomy Of A React Redux App
- Why Redux Need Reducers To Be “Pure Functions”
- Two Quick Ways To Reduce React App’s Size In Production
If you have questions, please feel free to ask me on Twitter: https://twitter.com/rajaraodv