First things first: Although this article and the related knowledge was developed with Vue.js in recent years, the basic findings can also be applied to React applications.
For many developers, getting started with Vue.js does not pose any insurmountable problems. In addition to excellent documentation that is constantly being expanded, there is also a large community on a wide variety of channels. However, if you decide for any reason to use server-side rendering (SSR), the complexity increases significantly. Fortunately, NuxtJS exists. A Vue.js framework, which has been growing steadily for several years now, provides a robust implementation of SSR, making it much easier to get started.
However, sooner or later you will encounter the following error messages in the Browser Console during development, whether or not you use NuxtJS:
Mismatching childNodes vs. VNodes
The client-side rendered virtual DOM tree is not matching server-rendered content.
HierarchyRequestError: Failed to execute ‘appendChild’ on ‘Node’: This node type does not support this method
The documentation included in the Vue SSR Guide describes the process as follows:
Hydration refers to the client-side process during which Vue takes over the static HTML sent by the server and turns it into dynamic DOM that can react to client-side data changes.
When the browser calls up a URL, the browser is issued the generated HTML on the basis of SSR and displays it appropriately.
The user receives a readable version of the web app even before the JavaScript execution in the browser is complete. What’s missing is the reactivity and DOM event handlers such as onclick. This step requires that the JavaScript still be executed in the browser in order for the web app to be fully usable.
In development mode, it is ensured that the virtual DOM generated on the client side matches the DOM structure rendered by the server. If there are discrepancies, the hydration is stopped and the existing DOM is discarded and re-rendered. To ensure better performance, this process is deactivated in production mode.
If a component is initially mounted or hydrated in the DOM, it needs to precisely correspond to the HTML generated on the server side. Only after this has happened may the HTML change. A suitable point in time for this after hydration would therefore be the mounted lifecycle.
The error can also only occur when using SSR and only if a URL is called directly, that is, HTML is returned from the server. This error will not occur with an application that is rendered exclusively on the client side.
Since hydration is only carried out when the page is first requested, no further errors occur after that and the website behaves like a pure single-page application.
As previously mentioned, it is ensured that the DOM tree generated in the browser matches the HTML provided by the server.
Vue will assert the client-side generated virtual DOM tree matches the DOM structure rendered from the server.
However, the following two HTML snippets produce a hydration error.
<!-- Server -->
<div>foo</div>
<!-- Client -->
<div><div>foo</div></div>
Differences in attributes or text nodes do not lead to errors.
<!-- Server -->
<div>foo</div>
<!-- Client -->
<div>bar</div>
The question now is how these differences can occur when it is the same Vue.js code being executed once on the server and once in the browser.
In general, there are two things that cause this to happen:
With today’s popular approach of component-driven development, it is easy to accidentally create invalid HTML across multiple components.
How does invalid HTML lead to hydration errors?
This is generally not a problem in regard to hydration errors. The problem is that during hydration we compare the virtual DOM created in the Vue.js application in the client with the DOM interpreted by the browser from the server’s HTML. The HTML generated by the SSR comes from the virtual DOM library by the name of snabbdom that is the same as in the client. The only difference is that the browser has processed the HTML even further. And, as is well known, browsers tend to forgive quite a large number of errors in the HTML structure – which may result in the same appearance visually, but the DOM is not identical.
A few examples:
<!-- Nested anchor tags -->
<a>foo<a>bar</a></a>
<!-- <div> in <p> -->
<p><div>foo</div></p>
<!-- <p> in <ul> -->
<ul><p>foo</p></ul>
<!-- v-if on root level -->
<template>
<div v-if="true">foo</div>
</template>
<!-- <table> without <tbody> -->
<table>
<tr>
<td>foo</td>
</tr>
</table>
Valid HTML needs to be returned at all times to keep the browser from adapting the DOM on its own. In the example above, awould have to be added to correct the hydration error.
Differences in HTML structure can arise for many different reasons, but in the end, it always revolves around the same problem:
We compare the result (HTML) from two calculations (rendering) at different times with different states (server, client).
It is important to note that these circumstances and this code must be present before hydration, and there must be differences in the HTML structure for this to come into question as a possible reason. As long as only the content of text nodes/attributes is affected, that is, no nodes are added or removed, hydration errors do not come about.
How do differences in state lead to hydration errors?If the server and client of the DOM have states that differ at the time of hydration, nodes may be missing or added, resulting in errors as described above.
Since the data function is executed again in the client, the following code would lead to hydration errors, with the possibility that a different value for displayContent than on the server could be determined. Then the DOM of the server and client would differ.
<!-- index.vue -->
<template>
<SomeContent v-if="displayContent" />
</template>
<script>
export default {
data() {
return {
displayContent: Math.random() > 0.5
}
}
}
</script>
Client & server indicators
Content is created depending on objects, variables, or values existing on the client or server side.
Random, location, or time/date
Contents are displayed randomly (Math.random()) or depending on location (navigator.geolocation) or time (new Date()), resulting in an HTML structure that differs when executed on the client and server side.
These values can be safely accessed after the mounted lifecycle since they are then only called in the client and after hydration has occurred.
Authentication
In SSR on-demand, display of user-specific content is often omitted in order to simplify caching. With static site generation, there is no access at all.
However, if user-specific content based on cookies, LocalStorage, login, or the like is rendered in the client before hydration, hydration errors can occur.
These values can be safely accessed after the mounted lifecycle since they are then only called in the client and after hydration has occurred.
Hash URLs and query
There are various scenarios in which components of a URL are not available in the SSR.
These values can be safely accessed after the mounted lifecycle since they are then only called in the client and after hydration has occurred.
Hash
If, contrary to the standard, the hash of a URL influences the content, then hydration errors can occur.
The value of the hash is not transmitted from the browser to the server, making it unavailable for the SSR or causing it to differ from the value during hydration in the browser.
Using the example of vue-router, it looks like this:
<!-- index.vue -->
<template>
<div>
<p v-if="$route.hash">foo</p>
</div>
</template>
Query
Another case is the use of query in the SSR. With static site generation, such as in NuxtJS, the query is always empty. This means the query content is “unsafe” and should not be used for rendering content.
Due to a caching model used by you, it is possible that you may want to ignore parts of the query in the SSR (such as tracking parameters) to ensure robust caching.
However, if this query influences the HTML in the client at the time of hydration, errors may result.
Third-party tools and HTML optimizations
Tools such as Optimizely and AB Tasty can change the DOM even before hydration is complete. However, support for these tools in regard to hydration is getting better and better.
Tools like HTMLMinifier or optimizers for Cloudflare such as Rocket Loader and AutoMinify can modify the server-side HTML in such a way that hydration errors can result.
State
If the content of a state (such as vuex, vue-apollo, vue-states) is responsible for the HTML structure of content, hydration errors can result.
This means the state generated in the SSR must be sent to the browsers so that the same data is available during hydration. If the state represented in the HTML differs between client and server, hydration errors result.
How can hydration errors caused by different states be corrected?
In a highly recommended blog post on hydration errors, TheAlexLichter described how to resolve inconsistent states.
Most problems can be solved by:
In his blog post, TheAlexLichter also addressed the issue of how to locate hydration errors.
In this way the cause of hydration errors can be found, especially in development environments.
In production environments, manual comparison of the HTML structure created with and without JavaScript sometimes helps as a last resort.
In contrast to the use of cURL, for example, by deactivating JavaScript and then copying the DOM, errors due to invalid HTML that are automatically corrected by the browser can be found. In the DOM comparison, you can then find errors such as a “tbody” that is missing in the DOM generated with JavaScript.
We are currently developing a tool internally to regularly use Puppeteer to check our websites in the CI for hydration errors. As soon as this tool is finished, we will post it on our GitHub profile.
We published a small challenge that allows you to apply the knowledge you have learned to fix hydration errors.