Subscribe on changes!

Hydration error when async component is used as app root

avatar
Mar 19th 2021

Version

3.0.7

Reproduction link

http://vue-next-example.arijs.org/

Steps to reproduce

Create SSR application and attempt hydration with an app that uses Async components.

What is expected?

Hydration occurs smoothy.

What is actually happening?

Error is thrown "Uncaught (in promise) TypeError: Cannot read property 'proxy' of null"


@posva

I searched here for this issue and I found the issue #2626 , but I already tried the recommended solution there and it didn't help.

I'm not using Vite (yet), this is pure Vue v3.0.7.

Before I was passing a Vue.defineAsyncComponent() to the VueRouter configuration, but I already replaced that with a function that return a promise that returns the component definition.

I also am enhancing the Vue.resolveComponent = function myOwnFunction() {...} that loads the components based on the name and this is returning a Vue.defineAsyncComponent() value.

The error is here:

  function createAppAPI(render, hydrate) {
      return function createApp(rootComponent, rootProps = null) {
      // ... snip ...
          const app = (context.app = {
          // ... snip ...
              mount(rootContainer, isHydrate) {
                  if (!isMounted) {
                  // ... snip ...
                      return vnode.component.proxy; // <== vnode.component is null

Below is the value of vnode:

({
  "__v_isVNode": true,
  "__v_skip": true,
  "type": {
    "name": "AsyncComponentWrapper",
    "setup": setup() {
      const instance = currentInstance; // already resolved if (resolvedComp) { return () => {…
    },
    "__asyncLoader": () => {…}
    "__proto__": Object
  },
  "props": null,
  "key": null,
  "ref": null,
  "scopeId": null,
  "children": null,
  "component": null,
  "suspense": null,
  "ssContent": null,
  "ssFallback": null,
  "dirs": null,
  "transition": null,
  "el": null,
  "anchor": null,
  "target": null,
  "targetAnchor": null,
  "staticCount": 0,
  "shapeFlag": 4,
  "patchFlag": 0,
  "dynamicProps": null,
  "dynamicChildren": null,
  "appContext": null
})

Before I was getting a warning from VueRouter saying that I couldn't use Vue.defineAsyncComponent() on the routes definition. After I changed it, I'm no longer getting that warning, but somehow I still have the vnode.component is null error.

avatar
Mar 19th 2021

Do you have the repository? If you can, make sure it's boiled down without the router

avatar
Mar 20th 2021

@posva @HcySunYang

Here it is, without VueRouter: http://vue-next-example.arijs.org/no-router/

And here's the git repo: https://github.com/arijs/vue-next-example

Thank you for looking into this.

avatar
Mar 21st 2021

Hey @rhengles , this is a bug, now you can’t use async components as your app's root.

Use cases that will fail:

const app = createSSRApp(defineAsyncComponent({/* ... */}))
app.mount('xxx')

As a workaround, you can use functional components or stateful components to wrap it:

const asyncComp = defineAsyncComponent({/* ... */})
const app = createSSRApp({ render: () => h(asyncComp) })
app.mount('xxx')
avatar
Mar 22nd 2021

It doesn't make sense to use an async component as the root of the app though because it's displayed right away. You should wait for the dynamic import instead and then create the app if the component is only known at runtime. Otherwise, you should really remove the dynamic import because it will have a negative impact in performance as the dynamic import would always take place

import(myDynamicComponentPath).then(component => {
  const app = createSSRApp(component)
  // ...
})
avatar
Mar 22nd 2021

@posva The sense is that I treat every single component as async in my app. I don't use the public instance returned from .mount() and I think most people don't either.

Don't worry about performance because when the app is prerendered, every component in the page is cached and the load is equivalent to Promise.resolve(component).

I could apply the workarounds suggested here, they're not hard. They just feel unelegant, perhaps because I don't understand the importance of only mounting sync components and not async components. If it is just because of the return of the public instance, I don't use that.

One of the key things that made me love Vue instead of React (and hating angular) is because I feel that Vue doesn't dictate how I should build my app, it just gives the tools and freedom to build it any way I like it.

You may say, "Well React is pretty much as unopinionated as Vue, it also gives you a lot of freedom", well the exception here is because of JSX. It pretty much requires a build step. It's possible to compile JSX on the front end, I've done it, but then you need to include the entire babel (or buble) library in your frontend, it is a lot of weight.

And don't even suggest to use the called "hyperscript" or writing "React.createElement()" calls by hand, that's very cumbersome. In contrast with that, using templates is closer to native html, simpler to write and to be parsed. Therefore Vue is the best option of a framework that has great documentation, tooling, support, performance, market share and flexibility.

I'm just sad that there's this subtle difference between components and async components and that they're not interchangeable, that async components are a second class citizen...

avatar
Mar 22nd 2021

The sense is that I treat every single component as async in my app.

You should absolutely avoid treating all components as async ones. Only do it for components that are not always displayed right away, e.g. a modal, or the v-else branch that contains a heavy component like an inline editor.

I'm just sad that there's this subtle difference between components and async components and that they're not interchangeable, that async components are a second class citizen...

Components and async components should have the same usage and be automatic like they are right now (which is what makes them 1st class citizens), but you cannot use an async component everywhere because you shouldn't in the first place.

I could apply the workarounds suggested here

To be clear, the part I'm suggesting is not a workaround. Don't do this:

createSSRApp(defineAsyncComponent(() => import('./MyComponent.vue)))

Do

import MyComponent from './MyComponent.vue'
createSSRApp(MyComponent)
// or createApp(MyComponent)

The root component should not be async

avatar
Mar 22nd 2021

@posva Thank you for your insightful input, and for listening and taking your time to respond.

I want to emphasize that you do not need to be concerned with performance, because when the app is shipped to production, they will be cached inside the app and the "load" won't have to make any network requests. So the async component will be resolved instantaneously for all intents and purposes, I just want the async API for the sake of simplicityness.

I'm not doing this:

createSSRApp(defineAsyncComponent(() => import('./MyComponent.vue)))

I'm doing this (simplified for illustration):

function resolveComponent(name) {
  const map = {};
  map["cached"] = {
    setup() {...},
    render() {...},
  };
  if (name in map) return Vue.defineAsyncComponent(() => Promise.resolve(map[name]));
}

createSSRApp(resolveComponent("cached"));

I simply want to be able to treat every component as async because that makes the code simpler and more elegant. There's no need to worry about performance.

I just don't understand the following statement:

The root component should not be async

Is there any reason for this, will something break other than the public instance returned by .mount() ?

avatar
Mar 22nd 2021

I just don't understand the following statement:

The root component should not be async

Is there any reason for this, will something break other than the public instance returned by .mount() ?

I explained it: it's not a good practice. Async components (as well as code splitting) are designed for the scenario I explained above with the example of modals.

avatar
Mar 22nd 2021

Please just give me my footgun, I promise not to point it any dangerous ways 😅

But see, I know what I'm doing, I would very much like that the framework i'm using would not fight me back and make my job harder.

If Vue wants to hand-hold novice users, maybe that enforcement should be made in a higher level, like vue-cli, vite-template-vue, etc. But the base library should not assume to know better than the user.

But maybe the correct solution here would be for me to write my own async components, this way they would always have an instance.