Skip to content

Commit a46feb2

Browse files
committed
docs: defining data loaders
1 parent 16d71ab commit a46feb2

File tree

3 files changed

+238
-10
lines changed

3 files changed

+238
-10
lines changed

docs/data-loaders/defining-loaders.md

Lines changed: 231 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import './typed-router.d'
1212
import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'
1313
import { getUserById } from '../api'
1414
15-
export const useUserData = defineBasicLoader('/users/[id]', async (route) => {
16-
return getUserById(route.params.id)
15+
export const useUserData = defineBasicLoader('/users/[id]', async (to) => {
16+
return getUserById(to.params.id)
1717
})
1818
</script>
1919
@@ -59,9 +59,9 @@ By using the route location to fetch data, we ensure a consistent relationship b
5959

6060
It's important to avoid side effects in the loader function. Don't call `watch`, or create reactive effects like `ref`, `toRefs()`, `computed`, etc.
6161

62-
## Accessing Global Properties
62+
### Global Properties
6363

64-
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**. Since loaders are asynchronous, make sure you are using the `inject` function **before any `await`**:
64+
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`**:
6565

6666
```ts twoslash
6767
import 'unplugin-vue-router/client'
@@ -70,7 +70,7 @@ import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'
7070
import { getUserById } from '../api'
7171
// ---cut---
7272
import { inject } from 'vue'
73-
import { useSomeStore } from '@/stores'
73+
import { useSomeStore, useOtherStore } from '@/stores'
7474

7575
export const useUserData = defineBasicLoader('/users/[id]', async (to) => {
7676
// ✅ This will work
@@ -79,8 +79,8 @@ export const useUserData = defineBasicLoader('/users/[id]', async (to) => {
7979

8080
const user = await getUserById(to.params.id)
8181
// ❌ These won't work
82-
const injectedValue2 = inject('key') // [!code error]
83-
const store2 = useSomeStore() // [!code error]
82+
const injectedValue2 = inject('key-2') // [!code error]
83+
const store2 = useOtherStore() // [!code error]
8484
// ...
8585
return user
8686
})
@@ -91,10 +91,232 @@ Why doesn't this work?
9191
// @error: Custom error message
9292
-->
9393

94+
### Navigation control
95+
96+
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
97+
98+
```ts{1,8,9}
99+
import { NavigationResult } from 'unplugin-vue-router/data-loaders'
100+
101+
const useDashboardStats = defineBasicLoader('/admin', async (to) => {
102+
try {
103+
return await getDashboardStats()
104+
} catch (err) {
105+
if (err.code === 401) {
106+
// same as returning '/login' in a navigation guard
107+
return new NavigationResult('/login')
108+
}
109+
throw err // unexpected error
110+
}
111+
})
112+
```
113+
114+
::: tip
115+
116+
Note that [lazy loaders](#lazy-loaders) cannot control the navigation since they do not block it.
117+
118+
:::
119+
120+
Read more in the [Navigation Aware](./navigation-aware.md) section.
121+
122+
### Errors
123+
124+
Any thrown Error will abort the navigation, just like in navigation guards. They will trigger the `router.onError` handler if defined.
125+
126+
::: tip
127+
128+
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.
129+
130+
:::
131+
132+
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.
133+
94134
## Options
95135

96-
All
136+
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.
137+
138+
### Non blocking loaders with `lazy`
139+
140+
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`.
141+
142+
```vue{10,16} twoslash
143+
<script lang="ts">
144+
// ---cut-start---
145+
import 'unplugin-vue-router/client'
146+
import './typed-router.d'
147+
import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'
148+
// ---cut-end---
149+
import { getUserById } from '../api'
150+
151+
export const useUserData = defineBasicLoader(
152+
'/users/[id]',
153+
async (to) => {
154+
const user = await getUserById(to.params.id)
155+
return user
156+
},
157+
{ lazy: true } // 👈 marked as lazy
158+
)
159+
</script>
160+
161+
<script setup>
162+
// Differently from the example above, `user.value` can and will be initially `undefined`
163+
const { data: user, isLoading, error } = useUserData()
164+
// ^?
165+
</script>
166+
167+
<!-- ... -->
168+
```
169+
170+
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.
171+
172+
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.
173+
174+
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:
175+
176+
```ts{6-7}
177+
export const useUserData = defineBasicLoader(
178+
async (to) => {
179+
// ...
180+
},
181+
{
182+
lazy: !import.env.SSR, // Vite specific
183+
}
184+
)
185+
```
186+
187+
You can even pass a function to `lazy` to determine if the loader should be lazy or not based on each load/navigation:
188+
189+
```ts{6-7}
190+
export const useSearchResults = defineBasicLoader(
191+
async (to) => {
192+
// ...
193+
},
194+
{
195+
// lazy if we are on staying on the same route
196+
lazy: (to, from) => to.name === from.name,
197+
}
198+
)
199+
```
200+
201+
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.
202+
203+
### Delaying data updates with `commit`
204+
205+
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.
206+
207+
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:
208+
209+
```ts twoslash
210+
import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'
211+
interface Book {
212+
title: string
213+
isbn: string
214+
description: string
215+
}
216+
function fetchBookCollection(): Promise<Book[]> {
217+
return {} as any
218+
}
219+
// ---cut---
220+
export const useBookCollection = defineBasicLoader(fetchBookCollection, {
221+
commit: 'immediate',
222+
})
223+
```
224+
225+
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.
226+
227+
### Server optimization with `server`
228+
229+
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.
230+
231+
```ts{3} twoslash
232+
import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'
233+
interface Book {
234+
title: string
235+
isbn: string
236+
description: string
237+
}
238+
function fetchRelatedBooks(id: string | string[]): Promise<Book[]> {
239+
return {} as any
240+
}
241+
// ---cut---
242+
export const useRelatedBooks = defineBasicLoader(
243+
(to) => fetchRelatedBooks(to.params.id),
244+
{ server: false }
245+
)
246+
```
247+
248+
You can read more about server side rendering in the [SSR](./ssr.md) section.
97249

98250
## Connecting a loader to a page
99251

100-
## Why
252+
The router needs to know what loaders should be ran with which page. This is achieved in two ways:
253+
254+
- **Automatically**: when a loader is exported from a page component that is lazy loaded, the loader will be automatically connected to the page
255+
256+
::: code-group
257+
258+
```ts{8} [router.ts]
259+
import { createRouter, createWebHistory } from 'vue-router'
260+
261+
export const router = createRouter({
262+
history: createWebHistory(),
263+
routes: [
264+
{
265+
path: '/settings',
266+
component: () => import('./settings.vue'),
267+
},
268+
],
269+
})
270+
```
271+
272+
```vue{3-5} [settings.vue]
273+
<script lang="ts">
274+
import { getSettings } from './api'
275+
export const useSettings = defineBasicLoader('/settings', async (to) =>
276+
getSettings()
277+
)
278+
</script>
279+
280+
<script lang="ts" setup>
281+
const { data: settings } = useSettings()
282+
</script>
283+
<!-- ...rest of the component -->
284+
```
285+
286+
:::
287+
288+
- **Manually**: by passing the defined loader into the `meta.loaders` property:
289+
290+
::: code-group
291+
292+
```ts{2,10-12} [router.ts]
293+
import { createRouter, createWebHistory } from 'vue-router'
294+
import Settings, { useSettings } from './settings.vue'
295+
296+
export const router = createRouter({
297+
history: createWebHistory(),
298+
routes: [
299+
{
300+
path: '/settings',
301+
component: Settings,
302+
meta: {
303+
loaders: [useSettings],
304+
},
305+
}
306+
],
307+
})
308+
```
309+
310+
```vue{3-5} [settings.vue]
311+
<script lang="ts">
312+
import { getSettings } from './api'
313+
export const useSettings = defineBasicLoader('/settings', async (to) =>
314+
getSettings()
315+
)
316+
</script>
317+
318+
<script lang="ts" setup>
319+
const { data: settings } = useSettings()
320+
</script>
321+
<!-- ...rest of the component -->
322+
```

docs/data-loaders/navigation-aware.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
## Consistent updates
66

77
## Lazy loaders
8+
9+
## Loading after the navigation
10+
11+
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**. It will eventually start loading when the page is mounted.

src/data-loaders/createDataLoader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export interface DefineDataLoaderOptionsBase<isLazy extends boolean> {
109109
server?: boolean
110110

111111
/**
112-
* When the data should be committed to the entry. This only applies to non-lazy loaders.
112+
* When the data should be committed to the entry. In the case of lazy loaders, the loader will try to commit the data
113+
* after all non-lazy loaders have finished loading, but it might not be able to if the lazy loader hasn't been
114+
* resolved yet.
113115
*
114116
* @see {@link DefineDataLoaderCommit}
115117
* @defaultValue `'after-load'`

0 commit comments

Comments
 (0)