Skip to content

Commit df7ef5d

Browse files
committed
feat(nuxt,kit,schema): add a factory function for useFetch and useAsyncData (#32300)
1 parent dcebd18 commit df7ef5d

29 files changed

Lines changed: 3071 additions & 676 deletions

docs/3.guide/5.recipes/3.custom-usefetch.md

Lines changed: 36 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,46 @@ The [`$fetch`](/docs/4.x/api/utils/dollarfetch) utility function (used by the [`
1010

1111
However, Nuxt provides a way to create a custom fetcher for your API (or multiple fetchers if you have multiple APIs to call).
1212

13-
## Custom `$fetch`
13+
## Recipe: API Client with Auth
1414

15-
Let's create a custom `$fetch` instance with a [Nuxt plugin](/docs/4.x/directory-structure/app/plugins).
15+
Let's say you have an external API at `https://api.nuxt.com` that requires JWT authentication via [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils), and you want to redirect to `/login` on `401` responses.
16+
17+
```ts [app/composables/useAPI.ts]
18+
export const useAPI = createUseFetch({
19+
baseURL: 'https://api.nuxt.com',
20+
onRequest ({ options }) {
21+
const { session } = useUserSession()
22+
if (session.value?.token) {
23+
options.headers.set('Authorization', `Bearer ${session.value.token}`)
24+
}
25+
},
26+
async onResponseError ({ response }) {
27+
if (response.status === 401) {
28+
await navigateTo('/login')
29+
}
30+
},
31+
})
32+
```
33+
34+
Now every call to `useAPI` automatically includes the auth header and handles 401 redirects:
35+
36+
```vue [app/pages/dashboard.vue]
37+
<script setup lang="ts">
38+
const { data: profile } = await useAPI('/me')
39+
const { data: orders } = await useAPI('/orders')
40+
</script>
41+
```
42+
43+
:read-more{to="/docs/4.x/api/composables/create-use-fetch"}
44+
45+
## Recipe: Custom `$fetch` Instance
46+
47+
If you need lower-level control, you can create a custom `$fetch` instance with a [Nuxt plugin](/docs/4.x/directory-structure/app/plugins) and either use it with `useAsyncData` directly or pass it to `createUseFetch`.
1648

1749
::note
1850
`$fetch` is a configured instance of [ofetch](https://github.com/unjs/ofetch) which supports adding the base URL of your Nuxt server as well as direct function calls during SSR (avoiding HTTP roundtrips).
1951
::
2052

21-
Let's pretend here that:
22-
- The main API is https://api.nuxt.com
23-
- We are storing the JWT token in a session with [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils)
24-
- If the API responds with a `401` status code, we redirect the user to the `/login` page
25-
2653
```ts [app/plugins/api.ts]
2754
export default defineNuxtPlugin((nuxtApp) => {
2855
const { session } = useUserSession()
@@ -31,7 +58,6 @@ export default defineNuxtPlugin((nuxtApp) => {
3158
baseURL: 'https://api.nuxt.com',
3259
onRequest ({ request, options, error }) {
3360
if (session.value?.token) {
34-
// note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
3561
options.headers.set('Authorization', `Bearer ${session.value?.token}`)
3662
}
3763
},
@@ -42,7 +68,6 @@ export default defineNuxtPlugin((nuxtApp) => {
4268
},
4369
})
4470

45-
// Expose to useNuxtApp().$api
4671
return {
4772
provide: {
4873
api,
@@ -51,7 +76,7 @@ export default defineNuxtPlugin((nuxtApp) => {
5176
})
5277
```
5378

54-
With this Nuxt plugin, `$api` is exposed from `useNuxtApp()` to make API calls directly from the Vue components:
79+
You can then use the custom `$fetch` instance directly:
5580

5681
```vue [app/app.vue]
5782
<script setup>
@@ -61,65 +86,9 @@ const { data: modules } = await useAsyncData('modules', () => $api('/modules'))
6186
```
6287

6388
::callout
64-
Wrapping with [`useAsyncData`](/docs/4.x/api/composables/use-async-data) **avoid double data fetching when doing server-side rendering** (server & client on hydration).
65-
::
66-
67-
## Custom `useFetch`/`useAsyncData`
68-
69-
Now that `$api` has the logic we want, let's create a `useAPI` composable to replace the usage of `useAsyncData` + `$api`:
70-
71-
```ts [app/composables/useAPI.ts]
72-
import type { UseFetchOptions } from 'nuxt/app'
73-
74-
export function useAPI<T> (
75-
url: string | (() => string),
76-
options?: UseFetchOptions<T>,
77-
) {
78-
return useFetch(url, {
79-
...options,
80-
$fetch: useNuxtApp().$api as typeof $fetch,
81-
})
82-
}
83-
```
84-
85-
Let's use the new composable and have a nice and clean component:
86-
87-
```vue [app/app.vue]
88-
<script setup>
89-
const { data: modules } = await useAPI('/modules')
90-
</script>
91-
```
92-
93-
If you want to customize the type of any error returned, you can also do so:
94-
95-
```ts
96-
import type { FetchError } from 'ofetch'
97-
import type { UseFetchOptions } from 'nuxt/app'
98-
99-
interface CustomError {
100-
message: string
101-
status: number
102-
}
103-
104-
export function useAPI<T> (
105-
url: string | (() => string),
106-
options?: UseFetchOptions<T>,
107-
) {
108-
return useFetch<T, FetchError<CustomError>>(url, {
109-
...options,
110-
$fetch: useNuxtApp().$api,
111-
})
112-
}
113-
```
114-
115-
::note
116-
This example demonstrates how to use a custom `useFetch`, but the same structure is identical for a custom `useAsyncData`.
89+
Wrapping with [`useAsyncData`](/docs/4.x/api/composables/use-async-data) **avoids double data fetching when doing server-side rendering** (server & client on hydration).
11790
::
11891

11992
:link-example{to="/docs/4.x/examples/advanced/use-custom-fetch-composable"}
12093

12194
:video-accordion{title="Watch a video about custom $fetch and Repository Pattern in Nuxt" videoId="jXH8Tr-exhI"}
122-
123-
::note
124-
We are currently discussing to find a cleaner way to let you create a custom fetcher, see https://github.com/nuxt/nuxt/issues/14736.
125-
::
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
title: 'createUseAsyncData'
3+
description: A factory function to create a custom useAsyncData composable with pre-defined default options.
4+
links:
5+
- label: Source
6+
icon: i-simple-icons-github
7+
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/asyncData.ts
8+
size: xs
9+
---
10+
11+
`createUseAsyncData` creates a custom [`useAsyncData`](/docs/4.x/api/composables/use-async-data) composable with pre-defined options. The resulting composable is fully typed and works exactly like `useAsyncData`, but with your defaults baked in.
12+
13+
::note
14+
`createUseAsyncData` is a compiler macro. It must be used as an **exported** declaration in the `composables/` directory (or any directory scanned by the Nuxt compiler). Nuxt automatically injects de-duplication keys at build time.
15+
::
16+
17+
## Usage
18+
19+
```ts [app/composables/useCachedData.ts]
20+
export const useCachedData = createUseAsyncData({
21+
getCachedData (key, nuxtApp) {
22+
return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key]
23+
},
24+
})
25+
```
26+
27+
```vue [app/pages/index.vue]
28+
<script setup lang="ts">
29+
const { data: mountains } = await useCachedData(
30+
'mountains',
31+
() => $fetch('https://api.nuxtjs.dev/mountains'),
32+
)
33+
</script>
34+
```
35+
36+
The resulting composable has the same signature and return type as [`useAsyncData`](/docs/4.x/api/composables/use-async-data), with all options available for the caller to use or override.
37+
38+
## Type
39+
40+
```ts [Signature]
41+
function createUseAsyncData (
42+
options?: Partial<AsyncDataOptions>,
43+
): typeof useAsyncData
44+
45+
function createUseAsyncData (
46+
options: (callerOptions: AsyncDataOptions) => Partial<AsyncDataOptions>,
47+
): typeof useAsyncData
48+
```
49+
50+
## Options
51+
52+
`createUseAsyncData` accepts all the same options as [`useAsyncData`](/docs/4.x/api/composables/use-async-data#params), including `server`, `lazy`, `immediate`, `default`, `transform`, `pick`, `getCachedData`, `deep`, `dedupe`, `timeout`, and `watch`.
53+
54+
See the full list of options in the [`useAsyncData` documentation](/docs/4.x/api/composables/use-async-data#params).
55+
56+
## Default vs Override Mode
57+
58+
### Default Mode (plain object)
59+
60+
When you pass a plain object, the factory options act as **defaults**. Callers can override any option:
61+
62+
```ts [app/composables/useLazyData.ts]
63+
export const useLazyData = createUseAsyncData({
64+
lazy: true,
65+
server: false,
66+
})
67+
```
68+
69+
```ts
70+
// Uses the defaults (lazy: true, server: false)
71+
const { data } = await useLazyData('key', () => fetchSomeData())
72+
73+
// Caller overrides server to true
74+
const { data } = await useLazyData('key', () => fetchSomeData(), { server: true })
75+
```
76+
77+
### Override Mode (function)
78+
79+
When you pass a function, the factory options **override** the caller's options. The function receives the caller's options as its argument:
80+
81+
```ts [app/composables/useStrictData.ts]
82+
// deep is always enforced as false
83+
export const useStrictData = createUseAsyncData(callerOptions => ({
84+
deep: false,
85+
}))
86+
```
87+
88+
:read-more{to="/docs/4.x/guide/recipes/custom-usefetch"}
89+
90+
:read-more{to="/docs/4.x/api/composables/use-async-data"}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
title: 'createUseFetch'
3+
description: A factory function to create a custom useFetch composable with pre-defined default options.
4+
links:
5+
- label: Source
6+
icon: i-simple-icons-github
7+
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/fetch.ts
8+
size: xs
9+
---
10+
11+
`createUseFetch` creates a custom [`useFetch`](/docs/4.x/api/composables/use-fetch) composable with pre-defined options. The resulting composable is fully typed and works exactly like `useFetch`, but with your defaults baked in.
12+
13+
::note
14+
`createUseFetch` is a compiler macro. It must be used as an **exported** declaration in the `composables/` directory (or any directory scanned by the Nuxt compiler). Nuxt automatically injects de-duplication keys at build time.
15+
::
16+
17+
## Usage
18+
19+
```ts [app/composables/useAPI.ts]
20+
export const useAPI = createUseFetch({
21+
baseURL: 'https://api.nuxt.com',
22+
})
23+
```
24+
25+
```vue [app/pages/modules.vue]
26+
<script setup lang="ts">
27+
const { data: modules } = await useAPI('/modules')
28+
</script>
29+
```
30+
31+
The resulting `useAPI` composable has the same signature and return type as [`useFetch`](/docs/4.x/api/composables/use-fetch), with all options available for the caller to use or override.
32+
33+
## Type
34+
35+
```ts [Signature]
36+
function createUseFetch (
37+
options?: Partial<UseFetchOptions>,
38+
): typeof useFetch
39+
40+
function createUseFetch (
41+
options: (callerOptions: UseFetchOptions) => Partial<UseFetchOptions>,
42+
): typeof useFetch
43+
```
44+
45+
## Options
46+
47+
`createUseFetch` accepts all the same options as [`useFetch`](/docs/4.x/api/composables/use-fetch#parameters), including `baseURL`, `headers`, `query`, `onRequest`, `onResponse`, `server`, `lazy`, `transform`, `getCachedData`, and more.
48+
49+
See the full list of options in the [`useFetch` documentation](/docs/4.x/api/composables/use-fetch#parameters).
50+
51+
## Default vs Override Mode
52+
53+
### Default Mode (plain object)
54+
55+
When you pass a plain object, the factory options act as **defaults**. Callers can override any option:
56+
57+
```ts [app/composables/useAPI.ts]
58+
export const useAPI = createUseFetch({
59+
baseURL: 'https://api.nuxt.com',
60+
lazy: true,
61+
})
62+
```
63+
64+
```ts
65+
// Uses the default baseURL
66+
const { data } = await useAPI('/modules')
67+
68+
// Caller overrides the baseURL
69+
const { data } = await useAPI('/modules', { baseURL: 'https://other-api.com' })
70+
```
71+
72+
### Override Mode (function)
73+
74+
When you pass a function, the factory options **override** the caller's options. The function receives the caller's options as its argument, so you can read them to compute your overrides:
75+
76+
```ts [app/composables/useAPI.ts]
77+
// baseURL is always enforced, regardless of what the caller passes
78+
export const useAPI = createUseFetch(callerOptions => ({
79+
baseURL: 'https://api.nuxt.com',
80+
}))
81+
```
82+
83+
This is useful for enforcing settings like authentication headers or a specific base URL that should not be changed by the caller.
84+
85+
## Combining with a Custom `$fetch`
86+
87+
You can pass a custom `$fetch` instance to `createUseFetch`:
88+
89+
```ts [app/composables/useAPI.ts]
90+
export const useAPI = createUseFetch({
91+
$fetch: useNuxtApp().$api as typeof $fetch,
92+
})
93+
```
94+
95+
:read-more{to="/docs/4.x/guide/recipes/custom-usefetch"}
96+
97+
:read-more{to="/docs/4.x/api/composables/use-fetch"}

docs/4.api/2.composables/use-async-data.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ const { data, status, pending, error, refresh, clear } = await useAsyncData(
2525
</script>
2626
```
2727

28-
::warning{to="/docs/4.x/guide/recipes/custom-usefetch#custom-usefetchuseasyncdata"}
29-
If you're using a custom `useAsyncData` wrapper, do not await it in the composable as that can cause unexpected behavior. See recipe for custom async data fetcher.
28+
::tip{to="/docs/4.x/guide/recipes/custom-usefetch#custom-usefetch-with-createusefetch"}
29+
Need a custom `useAsyncData` with pre-defined defaults? Use `createUseAsyncData` to create a fully typed custom composable. See the [custom useFetch recipe](/docs/4.x/guide/recipes/custom-usefetch) for details.
3030
::
3131

3232
::note

docs/4.api/2.composables/use-fetch.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ const { data, status, error, refresh, clear } = await useFetch('/api/modules', {
2525
</script>
2626
```
2727

28-
::warning{to="/docs/4.x/guide/recipes/custom-usefetch#custom-usefetchuseasyncdata"}
29-
If you're using a custom `useFetch` wrapper, do not await it in the composable as that can cause unexpected behavior. See recipe for custom async data fetcher.
28+
::tip{to="/docs/4.x/guide/recipes/custom-usefetch#custom-usefetch-with-createusefetch"}
29+
Need a custom `useFetch` with pre-defined defaults (like `baseURL` or auth headers)? Use `createUseFetch` to create a fully typed custom composable.
3030
::
3131

3232
::note
@@ -87,7 +87,7 @@ Keyed state created using `useFetch` can be retrieved across your Nuxt applicati
8787
::
8888

8989
::warning
90-
`useFetch` is a reserved function name transformed by the compiler, so you should not name your own function `useFetch`.
90+
`useFetch` is a reserved function name transformed by the compiler, so you should not name your own function `useFetch`. To create a custom variant with pre-defined options, use [`createUseFetch`](/docs/4.x/guide/recipes/custom-usefetch#custom-usefetch-with-createusefetch) instead.
9191
::
9292

9393
::warning
@@ -151,7 +151,6 @@ type UseFetchOptions<DataT> = {
151151
pick?: string[]
152152
$fetch?: typeof globalThis.$fetch
153153
watch?: MultiWatchSources | false
154-
timeout?: MaybeRefOrGetter<number>
155154
}
156155

157156
type AsyncDataRequestContext = {
@@ -193,7 +192,6 @@ type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
193192
| `body` | `MaybeRefOrGetter<RequestInit['body'] \| Record<string, any>>` | - | Request body. Objects are automatically stringified. |
194193
| `headers` | `MaybeRefOrGetter<Record<string, string> \| [key, value][] \| Headers>` | - | Request headers. |
195194
| `baseURL` | `MaybeRefOrGetter<string>` | - | Base URL for the request. |
196-
| `timeout` | `MaybeRefOrGetter<number>` | - | Timeout in milliseconds to abort the request. |
197195
| `cache` | `boolean \| string` | - | Cache control. Boolean disables cache, or use Fetch API values: `default`, `no-store`, etc. |
198196
| `server` | `boolean` | `true` | Whether to fetch on the server. |
199197
| `lazy` | `boolean` | `false` | If true, resolves after route loads (does not block navigation). |

packages/nuxt/build.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default defineBuildConfig({
1414
'head',
1515
'components',
1616
'pages',
17+
'compiler',
1718
].map(name => ({ input: `src/${name}/runtime/`, outDir: `dist/${name}/runtime`, format: 'esm', ext: 'js' } as BuildEntry)),
1819
],
1920
hooks: {

0 commit comments

Comments
 (0)