Basic Data Loader Example
User: {{ route.params.id }}
State
isLoading: {{ isLoading }}
Error: {{ error }}
{{ user == null ? String(user) : user }}
User: {{ route.params.id }}
isLoading: {{ isLoading }}
Error: {{ error }}
{{ user == null ? String(user) : user }}
User: {{ route.params.id }}
status: {{ status }}
isLoading: {{ isLoading }}
Error: {{ error }}
{{ user == null ? String(user) : user }}
Loading...
{{ error.message }}
{{ user }}
{{ error.message }}
``` If you want to be even stricter, you can override the default `Error` type with `unknown` (or anything else) by augmenting the `TypesConfig` interface. ```ts // types-extension.d.ts import 'unplugin-vue-router/data-loaders' export {} declare module 'unplugin-vue-router/data-loaders' { interface TypesConfig { Error: unknown } } ``` --- --- url: /guide/eslint.md --- # ESlint If you are not using auto imports, you will need to tell ESlint about `vue-router/auto-routes`. Add these lines to your eslint configuration: ```json{3} { "settings": { "import/core-modules": ["vue-router/auto-routes"] } } ``` ## `definePage()` Since `definePage()` is a global macro, you need to tell ESlint about it. Add these lines to your eslint configuration: ```json{3} { "globals": { "definePage": "readonly" } } ``` --- --- url: /guide/extending-routes.md --- # Extending Routes ## Extending routes in config You can extend the routes at build time with the `extendRoute` or the `beforeWriteFiles` options. Both can return a Promise: ````ts twoslash import VueRouter from 'unplugin-vue-router/vite' import path from 'node:path' /** * In ESM environments, you can use `import.meta.url` to get the current file path: * * ```ts * import { dirname } from 'node:path' * import { fileURLToPath } from 'node:url' * * const __filename = fileURLToPath(import.meta.url) * const __dirname = dirname(__filename) * ``` */ const __dirname: string = '...' // ---cut--- // @moduleResolution: bundler VueRouter({ extendRoute(route) { if (route.name === '/[name]') { route.addAlias('/hello-vite-:name') } }, beforeWriteFiles(root) { root.insert('/from-root', path.join(__dirname, './src/pages/index.vue')) }, }) ```` Routes modified this way will be reflected in the generated `typed-router.d.ts` file. ## In-Component Routing It's possible to override the route configuration directly in the page component file. These changes are picked up by the plugin and reflected in the generated `typed-router.d.ts` file. ### `definePage()` You can modify and extend any page component with the `definePage()` macro. This is useful for adding meta information, or modifying the route object. It's globally available in Vue components but you can import it from `unplugin-vue-router/runtime` if needed. ```vue{2-7} twoslash ``` If you are using ESLint, you will need [to declare it as a global variable](../guide/eslint.md#definepage). ::: danger You cannot use variables in `definePage()` as its passed parameter gets extracted at build time and is removed from ` ``` ```vue{2,4,9} [pages/[projectId]/insights.vue] ``` ::: In the example above, the `useProjectIssues` loader is defined in a separate file and imported in two different pages, `pages/[projectId]/issues.vue` and `pages/[projectId]/insights.vue`. They both use the same data but present it in a different way so there is no reason to create two different loaders for issues. By extracting the loader into a separate file, we ensure an optimal chunk split. When using this pattern, remember to **export the loader** in all the page components that use it. This is what allows the router to await the loader before rendering the page. ## Usage outside of page components Until now, we have only seen loaders used in page components. However, one of the benefits of using loaders is that they can be **reused in many parts of your application**, just like regular composables. This will not only eliminate code duplication but also ensure an optimal and performant data fetching by **deduplicating requests and sharing the data**. To use a loader outside of a page component, you can simply **import it** and use it like any other composable, without the need to export it. ```vue ``` ::: tip When using a loader in a non-page component, you must **export the loader** from the page components where it is used. If you only import and use the loader in a regular component, the router will not recognize it and won't trigger or await it during navigation. ::: ## Nested Routes When defining nested routes, you don't need to worry about exporting the loader in both the parent and the child components. This will be automatically optimized for you and the loader will be shared between the parent and the child components. Because of this, it's simpler to **always export data loaders** in the page component where **they are used**. --- --- url: /data-loaders/navigation-aware.md --- # Navigation aware Since the data fetching happens within a navigation guard, it's possible to control the navigation like in regular navigation guards: * Thrown errors (or rejected Promises) cancel the navigation (same behavior as in a regular navigation guard) and are intercepted by [Vue Router's error handling](https://router.vuejs.org/api/interfaces/router.html#onerror) * By returning a `NavigationResult`, you can redirect, cancel, or modify the navigation Any other returned value is considered as the *resolved data* and will appear in the `data` property. ## Controlling the navigation with `NavigationResult` `NavigationResult` is a class that can be returned or thrown from a loader to *change* the navigation. It accepts the same arguments as the [return value of a navigation guard](https://router.vuejs.org/guide/advanced/navigation-guards.html#Global-Before-Guards) **as long as it changes the navigation**. It doesn't accept `true` or `undefined` as these values do not change the navigation. ```ts{1,6-8,16,18} import { NavigationResult } from 'unplugin-vue-router/data-loaders' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' export const useUserData = defineBasicLoader( async (to) => { // cancel the navigation for invalid IDs if (isInvalidId(to.params.id)) { return new NavigationResult(false) } try { const user = await getUserById(to.params.id) return user } catch (error) { if (error.status === 404) { return new NavigationResult({ name: 'not-found' }) } else { throw error // aborts the router navigation } } } ) ``` ### Handling multiple navigation results Since navigation loaders can run in parallel, they can return different navigation results as well. In this case, you can decide which result should be used by providing a `selectNavigationResult()` method to the [`DataLoaderPlugin`](./index.md#installation): ```ts{3-6} twoslash import 'unplugin-vue-router/client' import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders' const app = createApp({}) const router = createRouter({ history: createWebHistory(), routes: [], }) // ---cut--- // @moduleResolution: bundler // @noErrors app.use(DataLoaderPlugin, { router, selectNavigationResult(results) { for (const { value } of results) { // If any of the results is a redirection to the not-found page, use it if ( typeof value === 'object' && 'name' in value && value.name === 'not-found' ) { return value } } }, }) ``` `selectNavigationResult()` is called with an array of all the returned `new NavigationResult(value)` **after all data loaders** have been resolved. **If any of them throws an error** or if none of them return a `NavigationResult`, `selectNavigationResult()` isn't called. By default, `selectNavigation` returns the first value of the array. ### Eagerly changing the navigation If a loader wants to eagerly alter the navigation, it can `throw` the `NavigationResult` instead of returning it. This skips the `selectNavigationResult()` and take precedence without triggering `router.onError()`. ```ts{11-16} import { NavigationResult } from 'unplugin-vue-router/data-loaders' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' export const useUserData = defineBasicLoader( async (to) => { try { const user = await getUserById(to.params.id) return user } catch (error) { throw new NavigationResult({ name: 'not-found', // keep the current path in the URL params: { pathMatch: to.path.split('/') }, query: to.query, hash: to.hash, }) } } ) ``` ## Consistent updates During a navigation, data loaders are grouped together like a *pack*. If the navigation is canceled, none of the results are used. This avoids having partial data updates in a page and inconsistencies between the URL and the page content. On the other hand, if the navigation is successful, all the data loaders are resolved together and the data is only updated **once all the loaders are resolved**. This is true even for lazy loaders. This ensures that even if you have loaders that are really fast, the old data is not displayed until all the loaders are resolved and the new data is completely ready to be displayed. ## Lazy loaders Apart from consistent updates, lazy loaders are not navigation-aware. They cannot control the navigation with errors or `NavigationResult`. They still start loading as soon as the navigation is initiated. ## Loading after the navigation It's possible to not start loading the data until the navigation is done. To do this, simply [**do not attach the loader to the page**](./defining-loaders.md#disconnecting-a-loader-from-a-page). It will eventually start loading when the page is mounted. --- --- url: /data-loaders/nested-loaders.md --- # Nested loaders Sometimes, requests depend on other fetched data (e.g. fetching additional user information). For these scenarios, we can simply import the other loaders and use them **within a different loader**: Call **and `await`** the loader inside the one that needs it, it will only be fetched once no matter how many times it is called during a navigation: ```ts twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' // ---cut--- // import the loader for user information import { useUserData } from './loaders/users' import { getCommonFriends, getCurrentUser } from './api' export const useUserCommonFriends = defineBasicLoader(async (route) => { // loaders must be awaited inside other loaders // . ‡ const user = await useUserData() // fetch other data const me = await getCurrentUser() const commonFriends = await getCommonFriends(me.id, user.id) return { ...user, commonFriends } }) ``` You will notice here that we have two different usages for `useUserData()`: * One that returns all the necessary information we need *synchronously* (not used here). This is the composable that we use in components * A second version that **only returns a promise of the data**. This is the version used within data loaders that enables sequential fetching. ## Nested invalidation Since `useUserCommonFriends()` loader calls `useUserData()`, if `useUserData()` is somehow *invalidated*, it will also automatically invalidate `useUserCommonFriends()`. This depends on the implementation of the loader and is not a requirement of the API. ::: warning Two loaders cannot use each other as that would create a *dead lock*. ::: This can get complex with multiple pages exposing the same loader and other pages using some of their *already exported* loaders within other loaders. But it's not an issue, **the user shouldn't need to handle anything differently**, loaders are still only called once: ```ts twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' // ---cut--- import { getFriends, getCommonFriends, getUserById, getCurrentUser, } from './api' export const useUserData = defineBasicLoader('/users/[id]', async (route) => { return getUserById(route.params.id) }) export const useCurrentUserData = defineBasicLoader( '/users/[id]', async (route) => { const me = await getCurrentUser() // imagine legacy APIs that cannot be grouped into one single fetch const friends = await getFriends(me.id) return { ...me, friends } } ) export const useUserCommonFriends = defineBasicLoader( '/users/[id]', async (route) => { const user = await useUserData() const me = await useCurrentUserData() const friends = await getCommonFriends(user.id, me.id) return { ...me, commonFriends: { with: user, friends } } } ) ``` In the example above we are exporting multiple loaders but we don't need to care about the order in which they are called nor try optimizing them because **they are only called once and share the data**. ::: danger **Caveat**: must call **and await** all nested loaders at the top of the parent loader (see `useUserData()` and `useCurrentUserData()`). You cannot put a different regular `await` in between. If you really need to await **anything that isn't a loader** in between, wrap the promise with `withDataContext()` to ensure the loader context is properly restored: ```ts{3} export const useUserCommonFriends = defineBasicLoader(async (route) => { const user = await useUserData() await withContext(functionThatReturnsAPromise()) const me = await useCurrentUserData() // ... }) ``` This allows nested loaders to be aware of their *parent loader*. This could probably be linted with an eslint plugin. It is similar to the problem `