|
1 |
| -# Why can't I use the current route inside `App.vue`? |
| 1 | +# Why can't I use the current route in `App.vue`? |
| 2 | + |
| 3 | +The short answer is *timing*. You can use the current route in `App.vue`, but the initial render will usually occur before the route is resolved. |
| 4 | + |
| 5 | +## Understanding the problem |
| 6 | + |
| 7 | +Let's imagine we have an application structured like this: |
| 8 | + |
| 9 | +``` |
| 10 | +App |
| 11 | ++- RouterView |
| 12 | + +- HomePage |
| 13 | +``` |
| 14 | + |
| 15 | +Here, `HomePage` is the route component for the current route, shown by the `RouterView`. |
| 16 | + |
| 17 | +Inside `HomePage` we might want to access the current route, maybe so we can access `params` or `query`. That's no problem, we can just use `useRoute()`: |
| 18 | + |
| 19 | +::: code-group |
| 20 | + |
| 21 | +```vue [Composition API] |
| 22 | +<!-- HomePage.vue --> |
| 23 | +<script setup> |
| 24 | +import { useRoute } from 'vue-router' |
| 25 | +
|
| 26 | +const route = useRoute() |
| 27 | +
|
| 28 | +console.log(route.params.id) |
| 29 | +</script> |
| 30 | +``` |
| 31 | + |
| 32 | +```vue [Options API] |
| 33 | +<!-- HomePage.vue --> |
| 34 | +<script> |
| 35 | +export default { |
| 36 | + created() { |
| 37 | + console.log(this.$route.params.id) |
| 38 | + } |
| 39 | +} |
| 40 | +</script> |
| 41 | +``` |
2 | 42 |
|
3 |
| -::: warning This answer is a stub. |
4 |
| -We are still working on writing the answers to the FAQ questions. The answer below is incomplete, but you may still find it useful. |
5 | 43 | :::
|
6 | 44 |
|
7 |
| -The short answer is *timing*. You can use the current route inside `App.vue`, but it won't be available in time for the initial render. |
| 45 | +If you're using the Options API then you'd use `this.$route` instead, but it works out much the same. |
| 46 | + |
| 47 | +If `HomePage` has child components then we can also use `useRoute()` or `this.$route` to access the current route in those components. |
| 48 | + |
| 49 | +But it gets a bit trickier if we're outside the route component, e.g. in `App.vue`. |
| 50 | + |
| 51 | +Resolving the route is not a synchronous process. [Navigation guards](https://router.vuejs.org/guide/advanced/navigation-guards.html) and [lazy-loaded components](https://router.vuejs.org/guide/advanced/lazy-loading.html) can be asynchronous, so the route won't be resolved until they are complete. Even if you aren't using those features, Vue Router uses promises internally, so the route still won't be resolved synchronously. |
| 52 | + |
| 53 | +When you first mount the application, `App` will render, but the route won't be resolved yet, so the `RouterView` will be empty. |
| 54 | + |
| 55 | +It is still possible to access `useRoute()` or `this.$route` during that initial render, but the route object will just be a placeholder. It won't yet contain the resolved values for the route. |
| 56 | + |
| 57 | +## A realistic example |
| 58 | + |
| 59 | +Consider the following example. It tries to use the current route in `App.vue` to decide which layout to wrap around the `RouterView`: |
| 60 | + |
| 61 | +```vue |
| 62 | +<!-- App.vue --> |
| 63 | +<script setup> |
| 64 | +import DefaultLayout from './layouts/DefaultLayout.vue' |
| 65 | +</script> |
| 66 | +
|
| 67 | +<template> |
| 68 | + <component :is="$route.meta.layout || DefaultLayout"> |
| 69 | + <RouterView /> |
| 70 | + </component> |
| 71 | +</template> |
| 72 | +``` |
| 73 | + |
| 74 | +During the initial render, the `$route.meta` won't be populated yet, so `layout` will just be `undefined`. It'll use the `DefaultLayout` for that initial render, no matter which route was accessed. This can lead to the page flashing, as it renders `DefaultLayout` and then jumps to an alternative layout once the route is resolved. |
| 75 | + |
| 76 | +There are a few ways we might fix this problem. |
| 77 | + |
| 78 | +## Deferring mounting |
| 79 | + |
| 80 | +Vue Router provides the method [`isReady()`](https://router.vuejs.org/api/interfaces/Router.html#isReady), which can be used to wait until it has resolved the route. We could use it to defer mounting the application until the route is ready: |
| 81 | + |
| 82 | +```js |
| 83 | +// main.js or main.ts |
| 84 | +// ... |
| 85 | + |
| 86 | +const app = createApp(App) |
| 87 | + |
| 88 | +app.use(router) |
| 89 | + |
| 90 | +router.isReady().then(() => { |
| 91 | + app.mount('#app') |
| 92 | +}) |
| 93 | +``` |
| 94 | + |
| 95 | +This approach is often used in SSR (server-side rendering), where both the server and client need to render the route component as part of the initial render. But it can also be used in applications without SSR. |
| 96 | + |
| 97 | +The potential problem with this approach is that it delays all rendering until the router is ready. If you're using SSR then this isn't likely to be a problem, as the user will see the server-generated content straight away and won't notice the small delay in hydration. But if you aren't using SSR, the Vue portion of your page will just be missing until it resolves. Most likely, your users will just see an empty page. |
| 98 | + |
| 99 | +Depending on your application, that might not matter. A brief pause before the page shows might be good enough, especially if the alternative is a page that flashes between layouts. |
| 100 | + |
| 101 | +## Extra handling in `App.vue` |
| 102 | + |
| 103 | +With a bit of extra effort, we could show a loading indicator if the route isn't resolved yet. There are a few ways we might implement this. One approach would be to use `router.isReady()` inside `App.vue` to track when the router is ready. e.g.: |
| 104 | + |
| 105 | +```vue |
| 106 | +<!-- App.vue --> |
| 107 | +<script setup> |
| 108 | +import { shallowRef } from 'vue' |
| 109 | +import { useRouter } from 'vue-router' |
| 110 | +import DefaultLayout from './layouts/DefaultLayout.vue' |
| 111 | +import LoadingIndicator from './components/LoadingIndicator.vue' |
| 112 | +
|
| 113 | +const router = useRouter() |
| 114 | +
|
| 115 | +const fallback = shallowRef(LoadingIndicator) |
| 116 | +
|
| 117 | +router.isReady().then(() => fallback.value = DefaultLayout) |
| 118 | +</script> |
| 119 | +
|
| 120 | +<template> |
| 121 | + <component :is="$route.meta.layout || fallback"> |
| 122 | + <RouterView /> |
| 123 | + </component> |
| 124 | +</template> |
| 125 | +``` |
| 126 | + |
| 127 | +Here we're using the `LoadingIndicator` component in place of the layout. The `LoadingIndicator` will ignore the slot content. |
| 128 | + |
| 129 | +We could also implement this using the slot of `RouterView`: |
| 130 | + |
| 131 | +```vue |
| 132 | +<!-- App.vue --> |
| 133 | +<script setup> |
| 134 | +import DefaultLayout from './layouts/DefaultLayout.vue' |
| 135 | +import LoadingIndicator from './components/LoadingIndicator.vue' |
| 136 | +</script> |
| 137 | +
|
| 138 | +<template> |
| 139 | + <RouterView v-slot="{ Component }"> |
| 140 | + <component v-if="Component" :is="$route.meta.layout || DefaultLayout"> |
| 141 | + <component :is="Component" /> |
| 142 | + </component> |
| 143 | + <LoadingIndicator v-else /> |
| 144 | + </RouterView> |
| 145 | +</template> |
| 146 | +``` |
| 147 | + |
| 148 | +The trick here is that the `Component` in the slot props will be `undefined` if the route hasn't resolved yet. |
0 commit comments