How to display a "loading" animation while a lazy-loaded route component is being loaded?

Decade Moon picture Decade Moon · Jul 3, 2017 · Viewed 18.6k times · Source

I have split my app into multiple chunks with webpack's code splitting feature so that the entire application bundle isn't downloaded when the user visits my webpage.

The chunks that some routes require can be reasonably large and may take a noticeable amount of time to download. This is fine, except the user is not aware that the page is actually loading when they click an internal link, so I need to somehow display a loading animation or something.

My router is configured like this:

[
  {
    path: '/',
    component: () => import(/* webpackChunkName: 'landing' */ './landing.vue'),
  },
  {
    path: '/foo',
    component: () => import(/* webpackChunkName: 'main' */ './foo.vue'),
  },
  {
    path: '/bar',
    component: () => import(/* webpackChunkName: 'main' */ './bar.vue'),
  },
]

Advanced Async Components in the Vue.js guide shows how to display a particular "loading" component while the component is being resolved -- this is exactly what I need, however it also says:

Note that when used as a route component in vue-router, these properties will be ignored because async components are resolved upfront before the route navigation happens.

How can I achieve this in vue-router? If this is not possible, lazily-loaded components would be pretty much useless to me because it would provide a poor experience to the user.

Answer

Linus Borg picture Linus Borg · Jul 3, 2017

You can use navigation guards to activate/deactivate a loading state that shows/hides a loading component:

If you would like to use something like "nprogress" you can do it like this:

http://jsfiddle.net/xgrjzsup/2669/

const router = new VueRouter({
  routes
})

router.beforeEach((to, from, next) => {
  NProgress.start()
  next()
})
router.afterEach(() => {
  NProgress.done()
})

Alternatively, if you want to show someting in-place:

http://jsfiddle.net/h4x8ebye/1/

Vue.component('loading',{ template: '<div>Loading!</div>'})

const router = new VueRouter({
  routes
})

const app = new Vue({
  data: { loading: false },
  router
}).$mount('#app')

router.beforeEach((to, from, next) => {
  app.loading = true
    next()
})

router.afterEach(() => {
  setTimeout(() => app.loading = false, 1500) // timeout for demo purposes
})

Then in the template:

<loading v-if="$root.loading"></loading>
  <router-view v-else></router-view>

That could also be easily encapsulated in a very small component instead of using the $root component for the loading state.