2

When I fill the component field in createRouter() like this:

{
    path: '/article/post',
    name: 'article-post',
    component: defineAsyncComponent({
      loader: () => import('@/views/article-post/index.vue'),
      loadingComponent: () => import('@/views/article-post/skeleton.vue'),
    }),
},

Seems like it's not working. What I want it to show a loading page when the actual page is loading.

How do I do that?

1
  • Possibly worth noting: in my case, it seems like doing a regular lazy loaded import route makes the homepage/root route render briefly while the correct route is loading. As such, doing this method, with defineAsyncComponent, let's me control what is shown as a loading fallback. Commented Nov 3, 2023 at 20:16

5 Answers 5

3

The loadingComponent value must be a component definition, and cannot itself be an async component:

import { createRouter } from 'vue-router'
import { defineAsyncComponent } from 'vue'
import Skeleton from '@/views/article-post/skeleton.vue'

export default createRouter({
  routes: [
    {
      path: '/article/post',
      name: 'article-post',
      component: defineAsyncComponent({
        loader: () => import('@/views/article-post/index.vue'),
        //loadingComponent: () => import('@/views/article-post/skeleton.vue'), ❌ cannot be dynamic import
        loadingComponent: Skeleton ✅
      }),
    },
  ]
})

Also note that the loading component (Skeleton) is only shown once, i.e., if the component definition is not already in cache. This Codesandbox adds an artifical delay in loader() to demonstrate.

Sign up to request clarification or add additional context in comments.

3 Comments

This answer is correct. But how about the warning in console, when I use defineAsyncComponent() in vue-router? vue-router.esm-bundler.js:71 [Vue Router warn]: Component "default" in record with path "/memos" is defined using "defineAsyncComponent()". Write "() => import('./MyPage.vue')" instead of "defineAsyncComponent(() => import('./MyPage.vue'))".
Right answer should not include defineAsyncComponent. Because Vue documentation states that "You should not use defineAsyncComponent when configuring route components with Vue Router".
Indeed, the route component should be a dynamic import of a wrapper component that contains the defineAsyncComponent for the target, as seen in this demo.
2

You can load components asynchronously like this:

{
    path: '/article/post',
    name: 'article-post',
    component: () => ({
        component: import('@/views/article-post/index.vue')
        loading: import('@/views/article-post/skeleton.vue')
    })
},

Comments

2

If you want to use defineAsyncComponent in vue-router and don't get warnings, you can use a wrapper like this:

import {defineAsyncComponent, h} from 'vue'
import Loading from 'shared/components/loading.vue'

function lazyLoad(component_name, params = {}) {
  return {
    render() {
      const async_component = defineAsyncComponent({
        loader: () => import(`pages/${component_name}`),
        loadingComponent: Loading,
        ...params
      })
      return h(async_component)
    }
  }
}

and then inject it to your route:

{
  path: '/',
  name: 'welcome',
  component: lazyLoad('Welcome')
}

Comments

0

Wanted to add to @Six Stringers answer but this was too long to fit in a comment.

That solution won't work if you have nested folders in like /pages/explore/Search.vue and want to pass lazyLoad("explore/Search"). It's something to do with how Vite (more specifically Rollup I think) handles these imports. Learn more about this, and potential fixes, in this issue. The cleanest one, I think, is this (with TypeScript):

const asyncRoute = (path: string) => {
  const component = defineAsyncComponent({
    loader: () =>
      import.meta
        .glob<false, string, Component>("../pages/**/*.vue")
        [`../pages/${path}.vue`](),
    loadingComponent: SomePlaceholderComponent,
  });
  return defineComponent({
    render() {
      return h(component);
    },
  });
};

Also notice one important difference. defineAsyncComponent is only called once.

In the original answer, it is called on every render(), so it's a new async component definition on every render. Not only is that probably bad for performance, it had a very unusual consequence for me...

I had a table of contents on a page, with hash links to different sections down the page, e.g. #faqs. When clicking on one of the hash links, the entire page would appear to reload/re-render (starting at <router-view />) and scroll to the top. I eventually tracked it down to this. I think what's happening is... it's expected that a re-render would happen, but since it's a completely new component definition, the re-render starts completely from scratch instead of updating only what changed (and preserving scroll position).

Comments

0

In my case Vue3.5 we couldn't use 'defineAsyncComponent' because of the warning. We couldn't use - UX was worse, it didn't changed route until current route (component) is fully loaded (bad for slow network users). We customized our async Loader ourselves: AsyncComponentLoader.ts

import {defineComponent,h,ref,shallowRef,onMounted,onUnmounted,} from'vue';
import RouterLoader from '@/components/shared/RouterLoader.vue';
import ErrorComponent from '@/components/shared/ErrorComp.vue';

interface AsyncComponentOptions {
  loader: () => Promise<any>;
  loadingComponent?: any;
  errorComponent?: any;
}

export function AsyncComponentLoader(options: AsyncComponentOptions) {
  const {
    loader,
    loadingComponent = RouterLoader,
    errorComponent = ErrorComponent,
  } = options;

  return defineComponent({
    name: 'AsyncComponentLoader',
    props: {},
    setup(props, { attrs }) {
      const component = shallowRef(loadingComponent);
      const error = shallowRef(null);
      const isMounted = ref(false); // Use a ref to track mounted state

  const loadComponent = async () => {
    component.value = loadingComponent;
    error.value = null;
    try {
      const module = await loader();
      const loadedComponent = module.default || module;
      if (isMounted.value) {
        component.value = loadedComponent;
      }
    } catch (err) {
      if (isMounted.value) {
        error.value = err;
        component.value = errorComponent;
        if (!errorComponent) {
          console.error(err);
        }
      }
    }
  };

  onMounted(() => {
    isMounted.value = true;
    loadComponent();
  });

  onUnmounted(() => {
    isMounted.value = false;
  });

  return () => (component.value
    ? h(component.value, { ...attrs, ...props })
    : null);
},

}); }

Then in 'routes.ts' define components:

const SomeComp = () => AsyncComponentLoader({
  loader: () => import(
    /* webpackChunkName: "sports" */
    '@/components/some-path/SomeComp.vue'
  ),
});

const OtherComp = () => AsyncComponentLoader({
  loader: () => import(
    /* webpackChunkName: "sports" */
    '@/components/some-path/OtherComp.vue'
  ),
});
const routes: RouteRecordRaw[] = [
  {
    path: routeMap.some.path,
    name: routeMap.some.name,
    component: SomeComp(),
    meta: {
      addToSitemap: true,
      appPath: true,
    },
  },
  {
    path: routeMap.app.path,
    name: routeMap.app.name,
    component: OtherComp(),
  },

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.