vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
47.43k stars 8.3k forks source link

Hydration detection #1723

Open CyberAP opened 4 years ago

CyberAP commented 4 years ago

What problem does this feature solve?

When an app is server rendered there is a case when server side state does not correspond to client side state on an init lifecycle. Take for example a window.matchMedia and conditional rendering depending on matching media queries, the actual media query matching happens after the created stage either on beforeMount or mounted.

If you take the mounted approach the downside is that client side rendered applications would also be forced into this mode of re-rendering after the initial render because there's no reliable way (at least that I am aware of) to detect that this component is being hydrated. With a beforeMount approach you'll certainly get hydration errors in production, which is a no go.

As a workaround your component can accept a prop like ssr or check for common SSR solutions presence like nuxt. Doing this for every isomorphic Vue library feels like a barrier for developers who want to support both client and server side rendering.

What does the proposed API look like?

Would it be possible to add a property on a component instance to indicate that in fact this component has hydration undergoing to make an informed decision what state update strategy to take?

{
  beforeMount() {
    if (this._hydrating) {
      bailout()
    } else {
      updateState()
    }
  }
}
SasanFarrokh commented 3 years ago

I think most of those who use SSR use the ClientOnly component (same as Nuxt) and they are using this implementation

// https://github.com/frandiox/vite-ssr/blob/master/src/vue/components.ts
export const ClientOnly = defineComponent({
  name: 'ClientOnly',
  setup(_, { slots }) {
    const show = ref(false)
    onMounted(() => {
      show.value = true
    })

    return () => (show.value && slots.default ? slots.default() : null)
  },
})

This feature is needed on these components too.

Here is what I tried to do:

// ClientOnly.ts
import { onMounted, defineComponent } from 'vue';

function onAfterHydration (fn: () => void) {
    if (typeof window === 'undefined') return;
    // @ts-ignore this is for checking if application is already mounted or not
    return document.querySelector('#app')?.__vue_app__ ? fn() : onMounted(fn);
}

export default defineComponent({
    setup (_, { slots }) {
        const show = ref(false);
        onAfterHydration(() => {
            show.value = true;
        });

        return () => (show.value && slots.default ? slots.default() : (slots.placeholder ? slots.placeholder() : null));
    },
});