stereobooster/react-snap

Update README about more Vuejs SSR hydration caveats

maxmilton opened this issue · 5 comments

When using react-snap with vue and webpack + vue-loader there's an extra client side hydration caveat worth adding to the readme.

When using react-snap with the minifyHtml.collapseWhitespace = true option vue client side hydration will fail because, by default, vue-loader includes whitespace when creating its virtual DOM representation. Since the VDOM and the real DOM are different (VDOM has extra whitespace textNodes), hydration fails.

Took me a while to work out what was going on but it turns out there's an easy solution. In the vue-loader configuration just add preserveWhitespace: false. In the official templates this is found in the vue-loader.conf.js file. Info in the vue-loader docs.

If that doesn't make sense please let me know and I'll try to explain better or create an example repo.

Update:
Vue uses empty comments as a marker when using v-if (<!----> when v-if == false or as a placeholder for async components/values). We need these empty comments in place so you need to make sure reactSnap.minifyHtml.removeComments is false.

Unforchunately this means you need to also implement your own comment removal logic which bloats the JS bundle a little. Example main.js with my normal window.snapSaveState function:

...

window.snapSaveState = () => {
  // enable client-side hydration once the page has been prerendered
  document.querySelector('#app').setAttribute('data-server-rendered', 'true');

  // remove scripts added to head by webpack; fix async race condition issue
  const scripts = document.head.getElementsByTagName('script');
  for (let i = 0; i < scripts.length; i += 1) { // forEach didn't work for some bizarre reason
    const el = scripts[i];

    if (el.async && el.charset === 'utf-8') {
      // console.warn('REMOVING:', el.src); // eslint-disable-line no-console
      el.remove();
    }
  }

  // remove comments (react-snap config can't take RegExp so do it custom)
  const html = document.documentElement;
  const noComments = html.innerHTML.replace(/<!--(?!\[if|-->)[\s\S]*?-->/g, '');
  html.innerHTML = noComments;
};

I've made a separate issue about allowing a JS config which would allow using reactSnap.minifyHtml.ignoreCustomComments = [/^$/] instead of the custom comment removal implementation.

forEach didn't work for some bizarre reason

because

The Element.getElementsByTagName() method returns a live HTMLCollection...
The HTMLCollection interface represents a generic collection (array-like object similar to arguments)

To use forEach you need to convert HTMLCollection to array.

remove scripts added to head by webpack; fix async race condition issue

This case should be handled by react-snap. If it is not the case for you, please open an issue.

Thanks for tips. I'm not an expert in Vue. I want to experiment with it a bit and add notes to Readme

Normally when dealing with a NodeList or HTMLCollection I do something like [...HTMLCollection].foreach(... but in my experimenting here it didn't work. I've seen some discussion about a babel transpile issue but it was easier to just use a good old loop. Anyway it's beside the point of this issue thread 🙃

I've opened another issue for the webpack bundle issue: #145

Once I have some free time I'll try to put together a PR with the proposed docs updates.

Fixed in #172