--- url: /data-loaders/basic.md --- # `defineBasicLoader()` Basic data loader that always reruns on navigation. ::: warning Data Loaders are experimental. Feedback is very welcome to shape the future of data loaders in Vue Router. ::: ## Setup ## Example ```vue ``` ## SSR ## Nuxt ## Unresolved Questions * Should this basic version also track what is used in the route object, like [Svelte Data Loaders do](https://kit.svelte.dev/docs/load#rerunning-load-functions)? --- --- url: /data-loaders/colada.md --- # `defineColadaLoader()` Loaders that use [@pinia/colada](https://github.com/posva/pinia-colada) under the hood. These loaders provide a more efficient way to have asynchronous state with cache, ssr support and more. The key used in these loaders are directly passed to `useQuery()` from `@pinia/colada` and are therefore invalidated by `useMutation()` calls. ::: warning Pinia Colada is Experimental (like data loaders). Feedback is very welcome to shape the future of data loaders in Vue Router. ::: ## Setup Follow the installation instructions in [@pinia/colada](https://github.com/posva/pinia-colada). ## Example ```vue ``` ::: tip If you are using unplugin-vue-router, you can pass a route name to `defineColadaLoader` to get typed routes in the `query` function. ```ts export const useUserData = defineColadaLoader('/users/[id]', { // ... }) ``` ::: ## Refresh by default To avoid unnecessary frequent refreshes, Pinia Colada refreshes the data when navigating (instead of *refetching*). Change the `staleTime` option to control how often the data should be refreshed, e.g. setting it to 0 will refresh the data every time the route changes. ## Route tracking The `query` function tracks what is used in the `to` parameter and will only refresh the data if **tracked** properties change. This means that if you use `to.params.id` in the `query` function, it will only refetch the data if the `id` parameter changes but not if other properties like `to.query`, `to.hash` or even `to.params.other` change. To make sure the data is updated, it will still refresh in these scenarios. Configure the `staleTime` option to control how often the data should be refreshed. ## SSR <--! Hydration does not trigger extra load \--> ## Nuxt ## Unresolved questions --- --- url: /data-loaders/apollo.md --- # Apollo WIP --- --- url: /data-loaders/load-cancellation.md --- # Cancelling a data loader Data loaders receive an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that can be passed to `fetch` and other Web APIs to cancel ongoing requests when the navigation is cancelled. If the navigation is cancelled because of errors or a new navigation, the signal aborts, causing any request using it to abort as well. ```ts twoslash interface Book { title: string isbn: string description: string } function fetchBookCollection(options: { signal?: AbortSignal }): Promise { return {} as any } // ---cut--- import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' export const useBookCollection = defineBasicLoader( async (_route, { signal }) => { return fetchBookCollection({ signal }) } ) ``` This aligns with the future [Navigation API](https://github.com/WICG/navigation-api#navigation-monitoring-and-interception) and other web APIs that use the `AbortSignal` to cancel an ongoing invocation. ## Best practices Depending on the data loader implementation, it might be more interesting **not** to cancel an ongoing request, for example, when using [Pinia Colada](./colada/), it might be more interesting to keep the request ongoing and cache the result for future navigations. Make sure to read the documentation --- --- url: /guide/configuration.md --- # Configuration Have a glimpse of all the existing configuration options with their corresponding **default values**: ```ts twoslash import type { TreeNode } from 'unplugin-vue-router/types' const myOwnGenerateRouteName = (routeNode: TreeNode) => { return 'ok' } import { isPackageExists as isPackageInstalled } from 'local-pkg' function getFileBasedRouteName(routeNode: TreeNode) { return 'ok' } // const process = { cwd: () => '' } // ---cut--- // @moduleResolution: bundler import VueRouter from 'unplugin-vue-router/vite' VueRouter({ // how and what folders to scan for files routesFolder: [ { src: 'src/pages', path: '', // override globals exclude: (excluded) => excluded, filePatterns: (filePatterns) => filePatterns, extensions: (extensions) => extensions, }, ], // what files should be considered as a pages extensions: ['.vue'], // what files to include filePatterns: ['**/*'], // files to exclude from the scan exclude: [], // where to generate the types dts: './typed-router.d.ts', // how to generate the route name getRouteName: (routeNode) => getFileBasedRouteName(routeNode), // default language for custom blocks routeBlockLang: 'json5', // how to import routes, can also be a string importMode: 'async', // where are paths relative to root: process.cwd(), // options for the path parser pathParser: { // should `users.[id]` be parsed as `users/:id`? dotNesting: true, }, // modify routes individually async extendRoute(route) { // ... }, // modify routes before writing async beforeWriteFiles(rootRoute) { // ... }, }) ``` ::: tip Highlight any of the options to see more details about it. ::: ## SSR It might be necessary to mark `vue-router` as `noExternal` in your `vite.config.js` in development mode: ```ts{7} import { defineConfig } from 'vite' import Vue from '@vitejs/plugin-vue' import VueRouter from 'unplugin-vue-router/vite' export default defineConfig(({ mode }) => ({ ssr: { noExternal: mode === 'development' ? ['vue-router'] : [], }, plugins: [VueRouter(), Vue()], })) ``` --- --- url: /data-loaders.md --- # Data Loaders Data loaders streamline any asynchronous state management with Vue Router, like **Data Fetching**. Adopting Data loaders ensures a consistent and efficient way to manage data fetching in your application. Keep all the benefits of using libraries like [Pinia Colada](./colada/) or [Apollo](./apollo/) and integrate them seamlessly with client-side navigation. This is achieved by extracting the loading logic **outside** of the component `setup` (unlike ``). This way, the loading logic can be executed independently of the component life cycle, and the component can focus on rendering the data. Data Loaders are automatically collected and awaited within a navigation guard, ensuring the data is ready before rendering the component. ## Features * Parallel data fetching and deduplication * Automatic loading state management * Error handling * Extensible by loader implementations * SSR support * Prefetching data support ## Installation After installing [unplugin-vue-router](../introduction.md), install the `DataLoaderPlugin` **before the `router`**. ```ts{12-15} twoslash import 'unplugin-vue-router/client' import './typed-router.d' // @moduleResolution: bundler // ---cut--- import { createApp } from 'vue' import { routes } from 'vue-router/auto-routes' import { createRouter, createWebHistory } from 'vue-router' import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders' // [!code ++] const router = createRouter({ history: createWebHistory(), routes, }) const app = createApp({}) // Register the plugin before the router app.use(DataLoaderPlugin, { router }) // [!code ++] // adding the router will trigger the initial navigation app.use(router) app.mount('#app') ``` ## Quick start There are different data loaders implementation, the most simple one is the [Basic Loader](./basic/) which always reruns data fetching. A more efficient one, is the [Colada Loader](./colada/) which uses [@pinia/colada](https://github.com/posva/pinia-colada) under the hood. In the following examples, we will be using the *basic loader*. Loaders are [composables](https://vuejs.org/guide/reusability/composables.html) defined through a `defineLoader` function like `defineBasicLoader` or `defineColadaLoader`. They are *used* in the component `setup` to extract the needed information. To get started, *define* and ***export*** a loader from a **page** component: ::: code-group ```vue{2,5-7,11-16} twoslash [src/pages/users/[id].vue] ``` ::: The loader will automatically run when the route changes, for example when navigating to `/users/1`, even when coming from `/users/2`, the loader will fetch the data and delay the navigation until the data is ready. On top of that, you are free to *reuse* the returned composable `useUserData` in any other component, and it will automatically share the same data fetching instance. You can even [organize your loaders in separate files](./organization.md) as long as you **export** the loader from a **page** component. ## Why Data Loaders? Data fetching is the most common need for a web application. There are many ways of handling data fetching, and they all have their pros and cons. Data loaders are a way to streamline data fetching in your application. Instead of forcing you to choose between different libraries, data loaders provide a consistent way to manage data fetching in your application no matter the underlying library or strategy you use. --- --- url: /data-loaders/rfc.md --- # Data Loaders * Start Date: 2022-07-14 * Target Major Version: Vue 3, Vue Router 4 * Reference Issues: - * [Discussion](https://github.com/vuejs/rfcs/discussions/460) * [Implementation PR](https://github.com/posva/unplugin-vue-router/tree/main/src/data-loaders) ## Todo List List of things that haven't been added to the document yet: * \[ ] Extendable API for data fetching libraries like vue-apollo, vuefire, vue-query, etc * \[ ] Warn if a non lazy loader is used without data: meaning it was used in a component without it being exported by a page component. Either make it lazy or export it ## Summary There is no silver bullet to data fetching because of the different data fetching strategies and how they can define the architecture of the application and its UX. However, I think it's possible to find a solution that is flexible enough to **promote good practices** and **reduce the complexity** of data fetching in applications. That is the goal of this RFC, to standardize and improve data fetching with vue-router: * Integrate data fetching to the navigation cycle * Blocks navigation while fetching or *defer* less important data (known as *lazy* in Nuxt) * Deduplicate requests * Delay data updates until all data loaders are resolved * Avoids displaying partially up-to-date data and inconsistent state * Configurable through a `commit` option * Optimal data fetching * Defaults to parallel fetching * Semantic sequential fetching if needed * Avoid `` * No cascading loading states * No double mounting * [more...](#suspense) * Provide atomic and global access to loading/error states * Allow 3rd party libraries to extend the loaders functionality by establish a set of Interfaces that can be implemented. This targets libraries like [VueFire](https://vuefire.vuejs.org), [@pinia/colada][pinia-colada], [vue-apollo](https://apollo.vuejs.org/), [@tanstack/vue-query][vue-query], etc to provide features like caching, pagination, etc. specific to their use cases. This proposal concerns Vue Router 4 and is implemented under [unplugin-vue-router][uvr]. This enables types in data loaders but **is not necessary**. This feature is independent of the rest of the plugin and can be used without it, namely **without file-based routing**. ::: tip In this RFC, data loaders are often referred as *loaders* for short. API names also use the word *loader* instead of *data loader* for brevity. πŸ’‘ Some of the examples are interactive: hover or tap on the code to see the types and other information. ::: ## Basic example We create data loaders with a `defineLoader()` function that returns a **composable that can be used in any component** (not only pages component). The loader is then picked up by a Navigation Guard. It can be attached to a page component in two ways: * Export the loader from the page component it is attached to. It must be lazy loaded (`() => import('~/pages/users-details.vue')`) * Manually add the loader to the route definition's `meta.loaders[]` Exported from a non-setup ` ``` When a loader is exported by the page component, it is **automatically** picked up as long as the route is **lazy loaded** (which is a best practice). If the route isn't lazy loaded, the loader can be directly defined in an array of data loaders on `meta.loaders`: ```ts twoslash import './shims-vue.d' // ---cut--- // @moduleResolution: bundler import { createRouter, createWebHistory } from 'vue-router' import UserList from './pages/UserList.vue' // could be anywhere import { useUserList, useUserData, type User } from './loaders/users' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/users', component: UserList, meta: { // Required when the component is not lazy loaded loaders: [useUserList], }, }, { path: '/users/:id', // automatically picks up all exported loaders component: () => import('./pages/UserDetails.vue'), }, ], }) ``` Regarding the returned values from `useUserData()`: * `data` (aliased to `user`), `isLoading`, and `error` are shallow ref and therefore reactive. * `reload` is a function that can be called to force a reload of the data without a new navigation. Note `useUserData()` can be used in **any component**, not only in the page component: just import the function and call it within ` ``` The page component might not even use `useUserData()` but we can still use it anywhere else: ```vue ``` ::: warning If you use a loader in a component while it wasn't exported by a page, it won't be awaited during navigation. This can lead to unexpected behavior but it can be caught during development with a warning. ::: ### TypeScript Types are automatically generated for the routes by [unplugin-vue-router][uvr] and can be referenced with the name of each route to hint `defineLoader()` the possible values of the current types. On top of that, `defineLoader()` infers the returned types: ```vue twoslash ``` The arguments can be removed during the compilation step in production mode since they are only used for types and are ignored at runtime. ### Non blocking data fetching (Lazy Loaders) Also known as [lazy async data in Nuxt](https://v3.nuxtjs.org/api/composables/use-async-data), loaders can be marked as lazy to **not block the navigation**. ```vue{10,16-17} twoslash ``` This patterns is useful to avoid blocking the navigation while *non critical data* is being fetched. It will display the page earlier while some of the parts of it are still loading and you are able to display loader indicators thanks to the `isLoading` property. Note this still allows for having different behavior during SSR and client side navigation, e.g.: if we want to wait for the loader during SSR but not during client side navigation: ```ts{6-7} export const useUserData = defineLoader( async (route) => { // ... }, { lazy: !import.env.SSR, // Vite lazy: process.client, // NuxtJS } ) ``` Existing questions: * [~~Should it be possible to await all pending loaders with `await allPendingLoaders()`? Is it useful for SSR? Otherwise we could always ignore lazy loaders in SSR. Do we need both? Do we need to selectively await some of them?~~](https://github.com/vuejs/rfcs/discussions/460#discussioncomment-3532011) * Should we be able to transform a loader into a lazy version of it: `const useUserDataLazy = asLazyLoader(useUserData)` ### AbortSignal The loader receives in a second argument access to an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that can be passed on to `fetch` and other Web APIs. If the navigation is cancelled because of errors or a new navigation, the signal aborts, causing any request using it to abort as well. ```ts twoslash import { defineBasicLoader as defineLoader } from 'unplugin-vue-router/data-loaders/basic' interface Book { title: string isbn: string description: string } function fetchBookCollection(options: { signal?: AbortSignal }): Promise { return {} as any } // ---cut--- export const useBookCollection = defineLoader(async (_route, { signal }) => { return fetchBookCollection({ signal }) }) ``` This aligns with the future [Navigation API](https://github.com/WICG/navigation-api#navigation-monitoring-and-interception) and other web APIs that use the `AbortSignal` to cancel an ongoing invocation. ### Implementations ### Interfaces Defining a minimal set of information and options for Data Loaders is what enables external libraries to implement their own data loaders. They are meant to extend these interfaces to add more features that are specific to them. You can see a practical example with the [Pinia Colada](./colada/) implementation. ::: danger This section is still a work in progress, see the [implementations](#implementations) instead. ::: ### Global API It's possible to access a global state of when data loaders are fetching (during navigation or when `reload()` is called) as well as when the data fetching navigation guard is running (only when navigating). * `isFetchingData: Ref`: is any loader currently fetching data? e.g. calling the `reload()` method of a loader * `isNavigationFetching: Ref`: is navigation being hold by a loader? (implies `isFetchingData.value === true`). Calling the `reload()` method of a loader doesn't change this. TBD: is this worth it? Are any other functions needed? ### Limitations * \~~Injections (`inject`/`provide`) cannot be used within a loader~~ They can now * Watchers and other composables shouldn't be used within data loaders: * if `await` is used before calling a composableΒ e.g. `watch()`, the scope **is not guaranteed** * In practice, **this shouldn't be a problem** because there is **no need** to create composables within a loader ## Drawbacks * At first, it looks less intuitive than just awaiting something inside `setup()` with `` [but it doesn't have its limitations](#suspense) and have many more features * Requires an extra ` ``` Or when params are involved in the data fetching: ```vue ``` This setup has many limitations: * Nested routes will force **sequential data fetching**: it's not possible to ensure an **optimal parallel fetching** * Manual data refreshing is necessary **unless you add a `key` attribute** to the `` which will force a remount of the component on navigation. This is not ideal because it will remount the component on every navigation, even when the data is the same. It's necessary if you want to do a `` but less flexible than the proposed solution which also works with a `key` if needed. * By putting the fetching logic within the `setup()` of the component we face other issues: * No abstraction of the fetching logic => **code duplication** when fetching the same data in multiple components * No native way to deduplicate requests among multiple components using them: it requires using a store and extra logic to skip redundant fetches when multiple components are using the same data * Does not block the navigation * We can block it by mounting the upcoming page component (while the navigation is still blocked by the data loader navigation guard) which can be **expensive in terms of rendering and memory** as we still need to render the old page while we ***try** to mount the new page*. * Cannot modify the output of the navigation (e.g. redirecting, cancelling, etc), if the fetching fails, we end up in an error state * No native way of caching data, even for very simple cases (e.g. no refetching when fast traveling back and forward through browser UI) * Not possible to precisely read (or write) the loading state (see [vuejs/core#1347](https://github.com/vuejs/core/issues/1347)]) On top of this it's important to note that this RFC doesn't limit you: you can still use Suspense for data fetching or other async state or even use both, **this API is completely tree shakable** and doesn't add any runtime overhead if you don't use it. Aligning with the progressive enhancement nature of Vue.js. ### Other alternatives * Allowing blocking data loaders to return objects of properties: ::: details ```ts export const useUserData = defineLoader(async (route) => { const user = await getUserById(route.params.id) // instead of return user return { user } }) // instead of const { data: user } = useUserData() const { user } = useUserData() ``` This was the initial proposal but since this is not possible with lazy loaders it was more complex and less intuitive. Having one single version is overall easier to handle. It does allow to return pending promises in the object that aren't awaited: ```ts export const useUserData = defineLoader(async (route) => { return { // awaited user: await getUserById(route.params.id) // not awaited, like lazy nonCriticalData: getNonCriticalData() // Promise<...> } }) ``` But this version overlaps with `lazy: true`. While semantically it would be more natural if it was defined with **one** loader, it limits the API to one loader per page and not being able to reuse the data, loading state, error, etc across pages and components, which also limits the extensibility. ::: * Adding a new ` ``` Too magical without clear benefit. ::: * Pass route properties instead of the whole `route` object: ::: details ```ts import { getUserById } from '../api' export const useUserData = defineLoader(async ({ params }) => { const user = await getUserById(params.id) return { user } }) ``` This has the problem of not being able to use the `route.name` to determine the correct typed params (with [unplugin-vue-router][uvr]): ```ts import { getUserById } from '../api' export const useUserData = defineLoader(async (route) => { if (route.name === 'user-details') { const user = await getUserById(route.params.id) // ^ typed! return { user } } }) ``` ::: * Naming ::: details Variables could be named differently and proposals are welcome: * `isLoading` -> `isPending`, `pending` (same as Nuxt) * Rename `defineLoader()` to `defineDataFetching()` (or others) ::: * Nested/Sequential Loaders drawbacks ::: details * Allowing `await getUserById()` could make people think they should also await inside ` ``` Note that lazy loaders can only control their own blocking mechanism. They can't control the blocking of other loaders. If multiple loaders are being used and one of them is blocking, the navigation will be blocked until all of the blocking loaders are resolved. A function could allow to conditionally block upon navigation: ```ts export const useUserData = defineLoader( loader, // ... { lazy: (route) => { // ... return true // or a number }, } ) ``` ::: * One could argue being able to reuse the result of loaders across any component other than page makes this more complex. Other frameworks expose a single *load* function from page components (SvelteKit, Remix) ## Adoption strategy Introduce this as part of [unplugin-vue-router][uvr] to test it first and make it part of the router later on. ## Unresolved questions * Integration with Server specifics in Frameworks like Nuxt: cookies, headers, server only loaders (can create redirect codes) * Should there by a `beforeLoad()` hook that is called and awaited before all data loaders * Same for `afterLoad()` that is always called after all data loaders * What else is needed besides the `route` inside loaders? * \~~Add option for placeholder data?~~ Data Loaders should implement this themselves * What other operations might be necessary for users? [uvr]: https://github.com/posva/unplugin-vue-router "unplugin-vue-router" [pinia-colada]: https://github.com/posva/pinia-colada "@pinia/colada" [vue-query]: https://tanstack.com/query/latest/docs/framework/vue/overview "@tanstack/vue-query" --- --- url: /data-loaders/defining-loaders.md --- # Defining Data Loaders In order to use data loaders, you need to define them first. Data loaders themselves are the composables returned by the different `defineLoader` functions. Each loader definition is specific to the `defineLoader` function used. For example, `defineBasicLoader` expects an async function as the first argument while `defineColadaLoader` expects an object with a `query` function. All loaders should allow to pass an async function that can throw errors, and `NavigationResult`. Any composables returned by *any* `defineLoader` function share the same signature: ```vue twoslash ``` **But they are not limited by it!** For example, the `defineColadaLoader` function returns a composable with a few more properties like `status` and `refresh`. Because of this it's important to refer to the documentation of the specific loader you are using. This page will guide you through the **foundation** of defining data loaders, no matter their implementation. ## The loader function The loader function is the *core* of data loaders. They are asynchronous functions that return the data you want to expose in the `data` property of the returned composable. ### The `to` argument The `to` argument represents the location object we are navigating to. It should be used as the source of truth for all data fetching parameters. ```ts twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' import { getUserById } from '../api' // ---cut--- export const useUserData = defineBasicLoader('/users/[id]', async (to) => { const user = await getUserById(to.params.id) // here we can modify the data before returning it return user }) ``` By using the route location to fetch data, we ensure a consistent relationship between the data and the URL, **improving the user experience**. ### Side effects It's important to avoid side effects in the loader function. Don't call `watch`, or create reactive effects like `ref`, `toRefs()`, `computed`, etc. ### Global Properties In the loader function, you can access global properties like the router instance, a store, etc. This is because using `inject()` within the loader function **is possible**, just like within navigation guards. Since loaders are asynchronous, make sure you are using the `inject` function **before any `await`**: ```ts twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' import { getUserById } from '../api' // ---cut--- import { inject } from 'vue' import { useSomeStore, useOtherStore } from '@/stores' export const useUserData = defineBasicLoader('/users/[id]', async (to) => { // βœ… This will work const injectedValue = inject('key') // [!code ++] const store = useSomeStore() // [!code ++] const user = await getUserById(to.params.id) // ❌ These won't work const injectedValue2 = inject('key-2') // [!code error] const store2 = useOtherStore() // [!code error] // ... return user }) ``` ### Navigation control Since loaders happen within the context of a navigation, you can control the navigation by returning a `NavigationResult` object. This is similar to returning a value in a navigation guard ```ts{1,8,9} import { NavigationResult } from 'unplugin-vue-router/data-loaders' const useDashboardStats = defineBasicLoader('/admin', async (to) => { try { return await getDashboardStats() } catch (err) { if (err.code === 401) { // same as returning '/login' in a navigation guard return new NavigationResult('/login') } throw err // unexpected error } }) ``` ::: tip Note that [lazy loaders](#lazy-loaders) cannot control the navigation since they do not block it. ::: Read more in the [Navigation Aware](./navigation-aware.md) section. ### Errors Any thrown Error will abort the navigation, just like in navigation guards. They will trigger the `router.onError` handler if defined. ::: tip Note that [lazy loaders](#lazy-loaders) cannot control the navigation since they do not block it, any thrown error will appear in the `error` property and not abort the navigation nor appear in the `router.onError` handler. ::: It's possible to define expected errors so they don't abort the navigation. You can read more about it in the [Error Handling](./error-handling.md) section. ## Options Data loaders are designed to be flexible and allow for customization. Despite being navigation-centric, they can be used outside of a navigation and this flexibility is key to their design. ### Non blocking loaders with `lazy` By default, loaders are *non-lazy*, meaning they will block the navigation until the data is fetched. But this behavior can be changed by setting the `lazy` option to `true`. ```vue{10,16} twoslash ``` This patterns is useful to avoid blocking the navigation while *non critical data* is being fetched. It will display the page earlier while lazy loaders are still loading and you are able to display loader indicators thanks to the `isLoading` property. Since lazy loaders do not block the navigation, any thrown error will not abort the navigation nor appear in the `router.onError` handler. Instead, the error will be available in the `error` property. Note this still allows for having different behavior during SSR and client side navigation, e.g.: if we want to wait for the loader during SSR but not during client side navigation: ```ts{6-7} export const useUserData = defineBasicLoader( async (to) => { // ... }, { lazy: !import.meta.env.SSR, // Vite specific } ) ``` You can even pass a function to `lazy` to determine if the loader should be lazy or not based on each load/navigation: ```ts{6-7} export const useSearchResults = defineBasicLoader( async (to) => { // ... }, { // lazy if we are on staying on the same route lazy: (to, from) => to.name === from.name, } ) ``` This is really useful when you can display the old data while fetching the new one and some of the parts of the page require the route to be updated like search results and pagination buttons. By using a lazy loader only when the route changes, the pagination can be updated immediately while the search results are being fetched, allowing the user to click multiple times on the pagination buttons without waiting for the search results to be fetched. ### Delaying data updates with `commit` By default, the data is updated only once all loaders are resolved. This is useful to avoid displaying partially loaded data or worse, incoherent data aggregation. Sometimes you might want to immediately update the data as soon as it's available, even if other loaders are still pending. This can be achieved by changing the `commit` option: ```ts twoslash import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' interface Book { title: string isbn: string description: string } function fetchBookCollection(): Promise { return {} as any } // ---cut--- export const useBookCollection = defineBasicLoader(fetchBookCollection, { commit: 'immediate', }) ``` In the case of [lazy loaders](#lazy-loaders), they also default to `commit: 'after-load'`. They will commit after all other non-lazy loaders if they can but since they are not awaited, they might not be able to. In this case, the data will be available when finished loading, which can be much later than the navigation is completed. ### Server optimization with `server` During SSR, it might be more performant to avoid loading data that isn't critical for the initial render. This can be achieved by setting the `server` option to `false`. That will completely skip the loader during SSR. ```ts{3} twoslash import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' interface Book { title: string isbn: string description: string } function fetchRelatedBooks(id: string | string[]): Promise { return {} as any } // ---cut--- export const useRelatedBooks = defineBasicLoader( (to) => fetchRelatedBooks(to.params.id), { server: false } ) ``` You can read more about server side rendering in the [SSR](./ssr.md) section. ## Connecting a loader to a page The router needs to know what loaders should be ran with which page. This is achieved in two ways: * **Automatically**: when a loader is exported from a page component that is lazy loaded, the loader will be automatically connected to the page ::: code-group ```ts{8} [router.ts] import { createRouter, createWebHistory } from 'vue-router' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/settings', component: () => import('./settings.vue'), }, ], }) ``` ```vue{3-5} [settings.vue] ``` ::: * **Manually**: by passing the defined loader into the `meta.loaders` property: ::: code-group ```ts{2,10-12} [router.ts] import { createRouter, createWebHistory } from 'vue-router' import Settings, { useSettings } from './settings.vue' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/settings', component: Settings, meta: { loaders: [useSettings], }, } ], }) ``` ```vue{3-5} [settings.vue] ``` ### *Disconnecting* a loader from a page It is also possible **not to connect a loader to a page**. This allows you to delay the loading until the component is mounted. Usually you want to start loading the data as soon as possible but in some cases, it might be better to wait until the component is mounted. This can be achieved by not exporting the loader from the page component. --- --- url: /data-loaders/error-handling.md --- # Error handling By default, all errors thrown in a loader are considered *unexpected errors*: they will abort the navigation, just like in a navigation guard. Because they abort the navigation, they will not appear in the `error` property of the loader. Instead, they will be intercepted by Vue Router's error handling with `router.onError()`. However, if the loader is **not navigation-aware**, the error cannot be intercepted by Vue Router and will be kept in the `error` property of the loader. This is the case for *lazy loaders* and [*reloading data*](./reloading-data.md). ## Defining expected Errors To be able to intercept errors in non-lazy loaders, we can specify a list of error classes that are considered *expected errors*. This allows blocking loader to **not abort the navigation** and instead keep the error in the `error` property of the loader and let the page locally display the error state. ```ts{3-10,14,18} twoslash import 'unplugin-vue-router/client' import './typed-router.d' // @moduleResolution: bundler // ---cut--- import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' // custom error class class MyError extends Error { // override is only needed in TS override name = 'MyError' // Displays in logs instead of 'Error' // defining a constructor is optional constructor(message: string) { super(message) } } export const useUserData = defineBasicLoader( async (to) => { throw new MyError('Something went wrong') // ... // ---cut-start--- return { name: 'John' } // ---cut-end--- }, { errors: [MyError], } ) ``` You can also specify *expected errors* globally for all loaders by providing the `errors` option to the `DataLoaderPlugin`. ```ts{4} twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { createApp } from 'vue' import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders' const app = createApp({}) const router = {} as any class MyError extends Error { name = 'MyError' constructor(message: string) { super(message) } } // @moduleResolution: bundler // ---cut--- app.use(DataLoaderPlugin, { router, // checks with `instanceof MyError` errors: [MyError], }) ``` Then you need to opt-in in the loader by setting the `errors` option to `true` to keep the error in the `error` property of the loader. ```ts{7} twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' // @moduleResolution: bundler // ---cut--- export const useUserData = defineBasicLoader( async (to) => { throw new Error('Something went wrong') // ... // ---cut-start--- return { name: 'John' } // ---cut-end--- }, { errors: true, } ) ``` ::: details Why is `errors: true` needed? One of the benefits of Data Loaders is that they ensure the `data` to be ready before the component is rendered. With expected errors, this is no longer true and `data` can be `undefined`: ```ts{11} twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic' // @moduleResolution: bundler // ---cut--- export const useDataWithErrors = defineBasicLoader( async (to) => { // ... // ---cut-start--- return { name: 'John' } // ---cut-end--- }, { errors: true, } ) const { data } = useDataWithErrors() data.value // `data` can be `undefined` ``` ::: ## Custom Error handling If you need more control over the error handling, you can provide a function to the `errors` option. This option is available in both the `DataLoaderPlugin` and when defining a loader. ```ts{3-9} twoslash import 'unplugin-vue-router/client' import './typed-router.d' import { createApp } from 'vue' import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders' const app = createApp({}) const router = {} as any // @moduleResolution: bundler // ---cut--- app.use(DataLoaderPlugin, { router, errors: (error) => { // Convention for custom errors if (error instanceof Error && error.name?.startsWith('My')) { return true } return false // unexpected error }, }) ``` ## Handling both, local and global errors TODO: this hasn't been implemented yet ## Error handling priority When you use both, global and local error handling, the local error handling has a higher priority and will override the global error handling. This is how the local and global errors are checked: * if local `errors` is `false`: abort the navigation -> `data` is not `undefined` * if local `errors` is `true`: rely on the globally defined `errors` option -> `data` is possibly `undefined` * else: rely on the local `errors` option -> `data` is possibly `undefined` ## TypeScript You will notice that the type of `error` is `Error | null` even when you specify the `errors` option. This is because if we call the `reload()` method (meaning we are outside of a navigation), the error isn't discarded, it appears in the `error` property **without being filtered** by the `errors` option. In practice, depending on how you handle the error, you will add a [type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) inside the component responsible for displaying an error or directly in a `v-if` in the template. ```vue-html ``` 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 `