If you wanna learn about ReactJS virtual DOM reconciliation there's probably no better way than actually building it yourself.
Please follow me on Twitter as I announce when new parts and videos are being published!
Before moving forward make sure you've read the intro to the series and the previous post on ReactJS clone - Creating DOM elements
React virtual DOM re-conciliation
Reconciliation in React is the process in which the real DOM is brought in line with the virtual DOM.
By reconciliation React figures out only the needed changes(the difference) that needs to be applied to the current DOM structure to make it identical to the virtual DOM.
A small change to the virtual DOM object
Before we begin with re-concilation algorithm there is a thing we need to change regarding our virtual DOM structure to bring it in line with React.
As discussed in section React and the Virtual DOM the current virtual DOM structure we're working with is:
const virtualDOM = {
type: "div",
props: { id: "id1", class: "abc" },
children: [
{
type: "div",
props: { class: "child" },
children: [],
},
],
};
We will be changing it so that the children key is part of the props
object:
const virtualDOM = {
type: "div",
props: {
id: "id1",
class: "abc",
children: [
{
type: "div",
props: { class: "child" },
children: [],
},
],
},
};
This part goals
In this part our end goal is to get the following code working properly without it having to fully re-create the real DOM each time:
const virtualDOM = {
type: "div",
props: { id: "id1", class: "abc" },
children: [
{
type: "div",
props: { class: "child" },
children: [],
},
],
};
const anotherVirtualDOM = {
type: "div",
props: { id: "id2", class: "xyz abc" },
children: [
{
type: "p",
props: { class: "child" },
children: [],
},
],
};
render(virtualDOM, document.getElementById("app"));
render(anotherVirtualDOM, document.getElementById("app"));
Notice how between the 2 renders, the root div
changes it's id and gets an additional class added.
Also the child div
is transformed into a p
tag.
We need to create an algorithm capable of identifying what has changed between 2 virtual trees and output that information as a list of changes.
That list of changes is then used by the render
function in react-dom.js
file to figure out how it should update the real DOM.
Also render
would need to know on which real DOM node the changes will need to be performed.
Refactoring the DOM elements creation
Up until now we've added support for rendering, based on an VDOM object a real DOM node.
Let's begin by refactoring the current code and start adding support to update the DOM based on a change
object that contains the instructions on how to update the DOM.
In react-dom.js
start by adding a new, internal function, called updateDOM
that looks somewhat like this:
const updateDOM = (change) => {};
Also, the render
method needs to change so that:
- It hands off the current virtual DOM and the next virtual DOM to a function capable of figuring out the differences between the 2 virtual DOMs
- For each change detected at step 1) it defers updating the DOM to the
updateDOM
function. This is needed so we can support multiple types of updates not just creating new DOM elements
To do that we will be introducing a new variable that holds the current virtual DOM.
At the top of the react-dom.js
file add:
// We will be mutating this variable. It starts out as null
let currentVirtualDOM = null;
Then change the render
method to look something like:
const render = (nextVirtualDOM, node) => {
if (!currentVirtualDOM) {
// This is the first render ever, empty the node container before rendering
node.innnerHTML = null;
}
// Detect the changes between the 2 virtual DOM trees
const changes = virtualDOMDiff(currentVirtualDOM, nextVirtualDOM, node);
// For each change update the REAL DOM
changes.forEach(updateDOM);
// Make the next virtual DOM the current one
currentVirtualDOM = nextVirtualDOM;
};
I believe the inline comments are pretty self-explanatory. Don't worry if it's not yet clear how virtualDOMDiff
is implemented or how updateDOM
works.
Types of DOM operations
Let's identify first which are the DOM operations that we could perform.
- Add new DOM nodes
When a certain node is missing from the old virtual DOM but it's present in the new one it should be created.
The change should obviously specify the type, the virtual DOM object and the parent node into which the newly created node will be inserted(domContextNode
)
{ type: 'create-node', vdom: virtualDOMNode, domContextNode }
- Remove node
Whenever a node was present in the old virtual DOM but is nowhere to be found in the new virtual DOM, then it must be removed from the real DOM.
The event should at the very minimum specify the type, the DOM node to be removed(node
property) and the parent of the node to be removed(domContextNode
property)
{
type: "remove-node", node, domContextNode;
}
- Adding and updating props prop
When a virtual DOM node contains a prop that is not present on the same node from the old virtual DOM.
Or when the property value has changed between 2 renders.
At minimum it would contain the type
, the name of the prop added or updated(prop
), the new value of the prop(value
) and the node on which the property
should be set or updated(domContextNode
).
{type: 'change-prop', prop: 'name', value: 'prop value', domContextNode }
- Removing a prop
When a prop does exist in an old virtual DOM but it does not exist on the new one
the property needs to be removed from DOM.
At a minimum the change should contain the type
, the name of the property to be removed(prop
field) and the DOM node from which the property should be removed(domContextNode
field).
{type: 'remove-prop', prop: 'name' domContextNode }
That pretty much covers it.
Processing changes
Now that we have defined the possible type of DOM changes it's time to actually code a function that takes in a change and applies it to the real DOM.
Just assume that the change objects are generated already for now. We will be coding that part shortly.
Let's code the updateDOM
function to handle all these changes:
const updateDOM = (change) => {
switch (change.type) {
case "create-node":
change.domContextNode.appendChild(createDOMElement(change.vdom));
break;
case "remove-node":
change.domContextNode.removeChild(change.node);
break;
case "change-prop":
change.domContextNode.setAttribute(change.prop, change.value);
break;
case "remove-prop":
change.domContextNode.removeAttribute(change.prop);
break;
default:
break;
}
};
So for each type of change we apply the changes to the real DOM using the regular DOM API.
Figuring out changes between two virtual DOMs
Now that we have a function capable of actually applying a change to the real DOM
is time to actually generate the list of changes.
We will delegate this responsability to a function named virtualDOMDiff
.
The virtualDOMDiff
function will act as an aggregator of all of the detected changes.
It's also probably a good time to put this re-conciliation logic into it's own file.
So go ahead and create a dom-reconciliation.js
file and put the definition of virtualDOMDiff
in it:
File: dom-reconciliation.js
const virtualDOMDiff = (virtualDOM, anotherVirtualDOM) => {
const differences = [];
// In here goes the diffing between virtualDOM and anotherVirtualDOM
return differences;
};
The function takes in the 2 virtual DOMs it needs to compare and outputs a list(an array) of changes between the 2 trees.
A change should be able to fully describe an operation that needs to be performed
on a current DOM element so it is brought in line with the latest virtual DOM.
There are 3 broad categories of things that can change between 2 virtual DOMs:
- Tag changes
- Detecting changed props
- Children changes
In the spirit of Single Responsibility Principle, we will be creating a function to deal with each of these type of changes.
Conclusion and next steps
We need to get smarted about hitting the real DOM each and every time the virtual DOM is updated.
As such we've began creating a smarter algorithm capable of surgically applying DOM changes based on the differences in virtual DOMs.
Next we will be implementing the part of the algorithm capable of detecting when a tag type has changed in any branch of the virtual DOM tree.
See the post ReactJS clone reconciliation algorithm - Detecting changed tags!