React's Virtual DOM is sold as an optimization bolted onto a declarative UI to make it fast. I tried to build the slow version, and learned that Virtual DOM is not an optimization, but rather a foundational building block for declarative UIs on the web.
React is popular tool for building web application User Interfaces ("UIs" or "Views"). With React, developers write code that declaratively specifies what the UI should look like at any given point, without worrying about how to change the interface between states.
Here's a React component:
What's notable about React code is what it doesn't do. The above code says nothing about how to:
…so this is cool. React components just "re-render" themselves any time their state changes, and React takes care of updating the DOM for you.
The DOM may be slow, but what happens if we ignore that and just delete the whole DOM and make a new one from scratch instead of messing around with Virtual DOMs? Can we get a declarative UI that works like React but slower?
Here is my attempt:
Seems to work!
There's a big problem with this technique, already showing in the click-counting button example above. See it? It's more obvious if we try to render a text input:
After you enter each character, the
<input> you're typing in gets deleted. It's replaced with a new one, and your cursor is not focused on it. Same with the click-counting
<button>: if you focus it and try to activate it multiple times with the space-bar, you'll find that you lose the button focus after each activation.
The broken demos above work correctly as specified, but they are unusable and feel broken because the semantics are wrong. Deleting the DOM and making a new one is like a page-refresh, so all transient UI state like
:focus, and even scroll position, are lost along with the DOM. Just like in a page-refresh.
When the data changes, React conceptually hits the "refresh" button, and knows to only update the changed parts.– Why React from the React documentation
Re-rendering in React is not like a page refresh, because none of that transient state is lost. The DOM is just mutated.
Does this difference in semantics matter? Probably not. Even React's own documentation gets it wrong. But thanks for reading anyway :)
React components render a description of what the DOM should look like, called the Virtual DOM. What I've learned is that Virtual DOM is an implementation detail of creating a declarative UI library, not an optimization.
Reacts killer speed feature is its nifty algorithms that operate on the Virtual DOM in order to quickly compute a small but sufficient number of DOM mutations to perform on the actual page to bring it up to date. That's it!
Thanks for reading. Let me know what you think, I'm @uncyclephil on twitter.
If I get around to it or if someone is interested, I've got a post in mind about another tricky problem for declarative web UIs: correctly dealing with recursive state updates triggered by DOM updates. Sounds so exciting I know…
Most HTML attributes can be set on DOM elements via
Element.setAttribute(attrName, value), but unfortunately,
value is special, and can't be set in this way. It has to be assigned to the element directly instead (
el.value = 'some text' instead of
el.setAttribute('value', 'some text')). The example code in the post ignores this special case for clarity. As far as I can find,
value is the only special attribute that can't be assigned via
Element.setAttribute. Friends, the web platform!
Special-case handling for looping
attrs might look like this:
The problem with the blow-the-dom-away approach above was that we lost transient state like
:focus (that we might not want to track ourselves) every time we replaced the DOM. While writing this post, it occurred to me: what if did track and control all that extra state?
Hey now we are getting somewhere! This is a declarative UI with close to page-refresh semantics, no Virtual DOM, and it seems to work!
…well, almost. We're not capturing some more important transient state: the cursor position! This is more quickly noticeable on Firefox, which places the cursor at the beginning of the input on
.focus(). Chrome puts it at the end, so you find the problem if you try to insert a word in the middle of some already-entered text. No reason we can't also manage and declare that state too though, right?
I'm not convinced that it's useful to go all the way with declarative UIs and manage every single bit of all the state in web applications.
blurevent triggered by removing an
<input />that had focus. Or should these events all be silenced if we're really going for page-refresh semantics?
So for the time being, I'm sticking to the simplest VirtualDOM implementation I can come up with.