skip to content

In Vue Router 5, what does app.use(router) start, and why would you await router.isReady() before calling app.mount()?

level: middleimportance: should knowfreq 50%

answer

  1. install does four jobs
  2. every navigation is async since v4
  3. START_LOCATION before the first finish
  4. empty first render, surprise enter animation
  5. rejects when the first navigation fails

basics

~20 s

app.use(router) registers RouterLink and RouterView, exposes $router and $route, and in the browser starts the asynchronous first navigation; awaiting isReady() mounts only after it finishes, avoiding an empty first render and a spurious enter transition.

solid answer

~40 s

The router's `install` registers `RouterLink` and `RouterView`, adds `$router` and `$route`, provides what `useRouter()` and `useRoute()` inject, and, when a `document` exists, pushes the current URL as the initial navigation. Since Vue Router 4 every navigation is asynchronous, including that first one: lazy route components load and guards run before the route is committed. Until then the current route is `START_LOCATION`, which matches nothing, so mounting immediately renders an empty `RouterView` first and a `<Transition>` around it plays an enter animation on load. `router.isReady()` returns a promise that resolves once the first navigation has finished, redirects included, and rejects if a guard throws or aborts it. The cost is that a slow guard delays the first paint, so it matters most for SSR and transitions. It replaced Vue Router 3's `router.onReady()`.

code

ts · 16 lines
ts
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'

const app = createApp(App)
app.use(router) // starts the first navigation in the browser

router
  .isReady()
  .then(() => app.mount('#app'))
  .catch((err) => {
    // a guard threw or aborted the first navigation
    console.error('initial navigation failed', err)
    app.mount('#app')
  })

go deeper

for a junior

Remember that app.use(router) starts the first navigation and that isReady() returns a promise you await before mount.

for a middle

Explain why the first navigation is asynchronous, what START_LOCATION renders, and when isReady() resolves or rejects.

for a senior

Weigh a blank screen while slow guards run against a first-render flash, and design the first guard so waiting for readiness stays cheap.

for a principal

Set a team rule for startup: which work may block the first paint, and which must move behind a placeholder or a cached session.

## What `app.use(router)` does The router object returned by `createRouter()` is a Vue plugin, and its `install` method does four jobs in Vue Router 5: 1. Registers `RouterLink` and `RouterView` as global components. 2. Adds `$router` and a reactive `$route` to `app.config.globalProperties`. 3. Provides the router and the current route to the app, which is what `useRouter()` and `useRoute()` inject. 4. **Starts the initial navigation**: it pushes the history's current location, but only when a `document` exists, only once even if several apps share the router, and only while the current route is still the start location. The router docs add the rule that `use()` must come before `mount()`, like any plugin. ## The first navigation is asynchronous Since Vue Router 4, **every navigation is asynchronous**, the first one included. Before a route is committed the router may: - run global `beforeEach` guards, per-route `beforeEnter` and in-component guards, - download lazy route components (`component: () => import(...)`), - run `beforeResolve` guards, - follow redirects, which start a new navigation. Until that completes, `router.currentRoute.value` is **`START_LOCATION`**, an exported constant whose path is `/` and whose `matched` list is empty. ## What goes wrong when you mount immediately - `RouterView` renders **nothing** on the first render, because no record matches the start location. - A `<Transition>` placed inside `RouterView`'s slot sees the page go from nothing to the first view, so it plays an **enter animation on load**, as if `appear` were set. - Layout decisions read from `route.meta`, such as "show the admin sidebar", flip after the navigation lands, producing a visible flash. - Code in a root component's `setup` that reads `route.params` or `route.query` sees the empty start values. None of this is an error; it is the gap between the first render and the first committed route. ## What `router.isReady()` promises - It returns a `Promise<void>` that **resolves when the initial navigation has finished**. Redirects from records or guards are followed first. - Once the router is ready it resolves immediately, so calling it again is cheap. - It **rejects** when the first navigation fails: a guard throws (the router also calls its `onError()` listeners) or a guard returns `false`, which rejects it with the aborted navigation failure. A later successful navigation makes the router ready for new callers. - It does **not** wait for rendering, for lazy components of other routes, or for data a view fetches after mounting. - On the server nothing starts the first navigation (there is no `document`), so the server entry must `push()` the request URL before awaiting; otherwise the promise never settles. ## The cost, and when to skip it Awaiting `isReady()` blocks the whole mount on the slowest part of the first navigation. If the admin app's global guard calls a session endpoint that takes two seconds, users stare at a blank page for two seconds. The migration guide's advice is to wait when you do **SSR** or rely on route **transitions**, and otherwise to consider mounting at once when initial guards are slow. Common compromises: - put a static loading indicator in `index.html` that the mounted app replaces, - mount immediately and render a placeholder while the router is still on `START_LOCATION`, - make the first guard cheap, for example by reading a cached session and revalidating later. ## Several apps, and unmounting One router instance may be installed in more than one app. The install hook tracks this: only the **first** install starts the initial navigation, so a second app does not trigger a second one. When the **last** app using the router is unmounted, the router resets itself: the current route goes back to `START_LOCATION`, its history listener is removed and it is marked not ready. Mounting a new app with the same router then starts a fresh initial navigation, and `isReady()` waits for that one. This matters in micro-frontend shells that mount and unmount the admin app on demand, and in tests that reuse a router across mounts. ## From Vue Router 3 | Vue Router 3 | Vue Router 5 | |---|---| | `router.onReady(ok, err)` | `router.isReady().then(ok).catch(err)` | | first navigation could complete synchronously | always asynchronous | | `new VueRouter()` plus `Vue.use(VueRouter)` | `createRouter()` plus `app.use(router)` |

  • The admin app's global guard calls the session endpoint on every navigation, and first paint got two seconds slower after adding isReady(). What would you change?
    Keep the guard but make its first run cheap: read a session cached from the previous visit and revalidate in the background, or check the session once before `app.use(router)` and let the guard read the result. Alternatively mount immediately with a placeholder while the router is still on `START_LOCATION`, accepting the extra render.
  • What happens to isReady() if the first navigation is redirected from /admin/ to /login by a guard?
    A redirect is not treated as a failure. The router starts a new navigation to `/login`, and `isReady()` resolves when that one is committed. Only a thrown error or an aborting `false` on the first navigation rejects the pending promise.

Opening a restaurant's doors before the first table is set: guests see an empty room, then everything appears at once. Awaiting isReady() means opening when the first table is ready, and if setting it fails, you find out before the doors open.

saying these in an interview costs you the question

  • The first navigation is synchronous, so the route is known right after app.use(router).
  • isReady() waits until RouterView has rendered its component into the DOM.
  • isReady() always resolves, even when a guard aborts the first navigation.
  • router.onReady(callback) still works in Vue Router 5.
  • Awaiting isReady() before mount costs nothing, so every app should do it.