Patching the Vue.js Virtual DOM: The need, the explanation and the solution

Your first question might well be, why would it be necessary to interfere with the Virtual DOM? Vue.js neatly abstracts this concept from us.

Just to quickly recap, when I say Virtual DOM, I’m talking about the DOM-like tree structure that Vue.js creates from your component templates. The documentation gives a good overview.

So for example, the following markup:

<div v-for=”item in list” :key=”item.id” @click="doSomething">

Might look like this in a render function:

this.list.map((item) => createElement(
"div",
{
key: item.id,
on: {
click: _vm.doSomething
}
}
));

Although not the exact same format, the object parameter above is the beginnings of a VNode (Virtual Node). These Virtual nodes are used to then create or patch the real DOM nodes.

The Need

In my application, I have a list view where the user can add new entries. Each time an entry is added, it is just a client-side record, not yet persisted to the server. The user will edit it as they wish and click save, at that point the application will send a request and if successful, will update the local data with something important… the server-side record’s unique ID.

This is a common pattern, here is a mockup to see what I am talking about.

In the above JSFiddle, the results panel shows a list, click add to add a new entry and then click save. For the example, the new ID is randomly generated, but the behaviour is the same. I have added a delay to the display of a newly mounted Item component, this allows us to see what is happening.

When the ID of the item record is updated, the existing component for this data is destroyed and a new one is created.

The Explanation

This happens because, within the parent Vue, the template’s v-for also has a key property (documentation) which is set to the ID of the record.

This is normal practice, normally the ID of a record is considered immutable, but this is a special scenario.

So when we change the ID value, it will trigger a re-render in the parent (due to reactivity), which will follow the Virtual DOM process.

The new render creates a hierarchy of VNodes and compares them with the existing VNodes to decide what DOM patches are necessary. Let’s imagine the following existing nodes:

VNodes: [
{ key: 'a', tag: 'span', text: 'hey there' },
{ key: 'b', ..... },
{ key: 'c', ..... },
]

And here are the results of a new render:

VNodes: [
{ key: 'a', tag: 'span', text: 'welcome' },
{ key: 'x', ..... },
{ key: 'c', ..... },
]

Now the DOM element matching the first VNode has its text updated, the element isn’t removed, just updated.

But the second DOM element is entirely removed and a new one created and inserted in the same position. A more costly operation, especially if the only change was the key (which as we know isn’t really supposed to change).

In my real world scenario, replacing the element is costly enough to be visible to the user, just as I have artificially done in my mockup.

No user wants to see a flicker of changing content.

The Solution

What we need to do to have the Virtual DOM process behave the right way for us, is to patch the current VNode held in memory, prior to a new render. Remember that each time a render happens the VNodes are kept, to be compared with the next set of VNodes from a subsequent render and only apply the patches to the DOM.

Most of you will know that a component has a property to access its DOM element ($el), what you may not know is that another non-documented property exists to access the VNode ($vnode). The documentation does, however, include a link to view the VNode Interface. This is all quite informally documented, no doubt to avoid overloading a developer who may not need to deep-dive.

The above JSFiddle is almost identical to the first, however, the save method now includes the line:

this.$vnode.key = newId;

Important that this line is before the item record is updated. It means the existing VNode has its key changed, then the data is updated, and reactivity means it will trigger a refresh.

But this time the record with a new ID will find an existing VNode with the right key, and the DOM will be patched (and the component kept).

Follow the same steps in this mockup of adding and saving an entry, you’ll notice there is no flicker of a removed and added element, because it doesn’t happen.