mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 09:25:54 +00:00
feat(nuxt3): useFetch
(#721)
This commit is contained in:
parent
ad821d1aba
commit
54c57e3987
@ -1,10 +1,10 @@
|
|||||||
# Data Fetching
|
# Data Fetching
|
||||||
|
|
||||||
Nuxt provides `useAsyncData` to handle data fetching within your application.
|
Nuxt provides `useFetch` and `useAsyncData` to handle data fetching within your application.
|
||||||
|
|
||||||
## `useAsyncData`
|
## `useAsyncData`
|
||||||
|
|
||||||
Within your pages and components you can use `useAsyncData` to get access to data that resolves asynchronously.
|
Within your pages, components and plugins you can use `useAsyncData` to get access to data that resolves asynchronously.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
@ -17,12 +17,19 @@ useAsyncData(key: string, fn: () => Object, options?: { defer: boolean, server:
|
|||||||
* **options**:
|
* **options**:
|
||||||
- _defer_: whether to load the route before resolving the async function (defaults to `false`)
|
- _defer_: whether to load the route before resolving the async function (defaults to `false`)
|
||||||
- _server_: whether the fetch the data on server-side (defaults to `true`)
|
- _server_: whether the fetch the data on server-side (defaults to `true`)
|
||||||
|
- _transform_: A function that can be used to alter fn result after resolving
|
||||||
|
- _pick_: Only pick specified keys in this array from fn result
|
||||||
|
|
||||||
Under the hood, `defer: false` uses `<Suspense>` to block the loading of the route before the data has been fetched. Consider using `defer: true` and implementing a loading state instead for a snappier user experience.
|
Under the hood, `defer: false` uses `<Suspense>` to block the loading of the route before the data has been fetched. Consider using `defer: true` and implementing a loading state instead for a snappier user experience.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```vue
|
```js [server/api/index.ts]
|
||||||
|
let counter = 0
|
||||||
|
export default () => ++counter
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue [pages/index.vue]
|
||||||
<script setup>
|
<script setup>
|
||||||
const { data } = await useAsyncData('time', () => $fetch('/api/count'))
|
const { data } = await useAsyncData('time', () => $fetch('/api/count'))
|
||||||
</script>
|
</script>
|
||||||
@ -32,6 +39,42 @@ const { data } = await useAsyncData('time', () => $fetch('/api/count'))
|
|||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `useFetch`
|
||||||
|
|
||||||
|
Within your pages, components and plugins you can use `useFetch` to get universally fetch from any URL.
|
||||||
|
|
||||||
|
This composable provides a convenient wrapper around `useAsyncData` and `$fetch` and automatically generates a key based on url and fetch options and infers API response type.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
useFetch(url: string, options?)
|
||||||
|
```
|
||||||
|
|
||||||
|
Available options:
|
||||||
|
- `key`: Provide a custom key
|
||||||
|
- Options from [ohmyfetch](https://github.com/unjs/ohmyfetch)
|
||||||
|
- `method`: Request method
|
||||||
|
- `params`: Query params
|
||||||
|
- `baseURL`: Base URL for request
|
||||||
|
- Options from `useAsyncDaa`
|
||||||
|
- `defer`
|
||||||
|
- `server`
|
||||||
|
- `pick`
|
||||||
|
- `transform`
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```vue [pages/index.vue]
|
||||||
|
<script setup>
|
||||||
|
const { data } = await useFetch('/api/count')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
Page visits: {{ data.count }}
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
### Best practices
|
### Best practices
|
||||||
|
|
||||||
As seen in [Concepts > Data fetching](/concepts/data-fetching), the data returned by `useAsyncData` will be stored inside the page payload. This mean that every key returned that is not used in your component will be added to the payload.
|
As seen in [Concepts > Data fetching](/concepts/data-fetching), the data returned by `useAsyncData` will be stored inside the page payload. This mean that every key returned that is not used in your component will be added to the payload.
|
||||||
@ -54,13 +97,11 @@ Imagine that `/api/mountains/everest` returns the following object:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you plan to only use `title` and `description` in your component, you can select the keys by chaining the result of `$fetch`:
|
If you plan to only use `title` and `description` in your component, you can select the keys by chaining the result of `$fetch` or `pick` option:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
const { data: mountain } = await useAsyncData('everest', () =>
|
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
|
||||||
$fetch('/api/mountains/everest').then(({ title, description }) => ({ title, description }))
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
4
examples/use-fetch/nuxt.config.ts
Normal file
4
examples/use-fetch/nuxt.config.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { defineNuxtConfig } from 'nuxt3'
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
})
|
12
examples/use-fetch/package.json
Normal file
12
examples/use-fetch/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "example-use-fetch",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"nuxt3": "latest"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"start": "node .output/server/index.mjs"
|
||||||
|
}
|
||||||
|
}
|
9
examples/use-fetch/pages/index.vue
Normal file
9
examples/use-fetch/pages/index.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Server response: {{ data }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const { data } = await useFetch('/api/hello', { params: { foo: 'bar' } })
|
||||||
|
</script>
|
3
examples/use-fetch/server/api/hello.ts
Normal file
3
examples/use-fetch/server/api/hello.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { useQuery } from 'h3'
|
||||||
|
|
||||||
|
export default req => ({ query: useQuery(req) })
|
@ -4,7 +4,8 @@ import autoImports from '../../nuxt3/src/auto-imports/module'
|
|||||||
// TODO: implement these: https://github.com/nuxt/framework/issues/549
|
// TODO: implement these: https://github.com/nuxt/framework/issues/549
|
||||||
const disabled = [
|
const disabled = [
|
||||||
'useAsyncData',
|
'useAsyncData',
|
||||||
'asyncData'
|
'asyncData',
|
||||||
|
'useFetch'
|
||||||
]
|
]
|
||||||
|
|
||||||
const identifiers = {
|
const identifiers = {
|
||||||
|
@ -55,7 +55,6 @@ export async function build (nitroContext: NitroContext) {
|
|||||||
|
|
||||||
nitroContext.rollupConfig = getRollupConfig(nitroContext)
|
nitroContext.rollupConfig = getRollupConfig(nitroContext)
|
||||||
await nitroContext._internal.hooks.callHook('nitro:rollup:before', nitroContext)
|
await nitroContext._internal.hooks.callHook('nitro:rollup:before', nitroContext)
|
||||||
await writeTypes(nitroContext)
|
|
||||||
return nitroContext._nuxt.dev ? _watch(nitroContext) : _build(nitroContext)
|
return nitroContext._nuxt.dev ? _watch(nitroContext) : _build(nitroContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +88,7 @@ async function writeTypes (nitroContext: NitroContext) {
|
|||||||
|
|
||||||
async function _build (nitroContext: NitroContext) {
|
async function _build (nitroContext: NitroContext) {
|
||||||
nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir)
|
nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir)
|
||||||
|
await writeTypes(nitroContext)
|
||||||
|
|
||||||
consola.start('Building server...')
|
consola.start('Building server...')
|
||||||
const build = await rollup(nitroContext.rollupConfig).catch((error) => {
|
const build = await rollup(nitroContext.rollupConfig).catch((error) => {
|
||||||
@ -151,4 +151,5 @@ async function _watch (nitroContext: NitroContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
await writeTypes(nitroContext)
|
||||||
}
|
}
|
||||||
|
6
packages/nitro/types/fetch.d.ts
vendored
6
packages/nitro/types/fetch.d.ts
vendored
@ -23,9 +23,9 @@ export declare type TypedInternalResponse<Route, Default> =
|
|||||||
: MiddlewareOf<Route>
|
: MiddlewareOf<Route>
|
||||||
: Default
|
: Default
|
||||||
|
|
||||||
export declare interface $Fetch {
|
export declare interface $Fetch<T = unknown, R extends FetchRequest = FetchRequest> {
|
||||||
<T = unknown, R extends FetchRequest = FetchRequest> (request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
|
(request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
|
||||||
raw<T = unknown, R extends FetchRequest = FetchRequest> (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
|
raw (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -2,24 +2,44 @@ import { onBeforeMount, onUnmounted, ref } from 'vue'
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { NuxtApp, useNuxtApp } from '#app'
|
import { NuxtApp, useNuxtApp } from '#app'
|
||||||
|
|
||||||
export interface AsyncDataOptions<T> {
|
export type _Transform<Input=any, Output=any> = (input: Input) => Output
|
||||||
|
|
||||||
|
export type PickFrom<T, K extends Array<string>> = T extends Record<string, any> ? Pick<T, K[number]> : T
|
||||||
|
export type KeysOf<T> = Array<keyof T extends string ? T : string>
|
||||||
|
export type KeyOfRes<Transform extends _Transform> = KeysOf<ReturnType<Transform>>
|
||||||
|
|
||||||
|
export interface AsyncDataOptions<
|
||||||
|
DataT,
|
||||||
|
Transform extends _Transform<DataT, any> = _Transform<DataT, DataT>,
|
||||||
|
PickKeys extends KeyOfRes<_Transform> = KeyOfRes<Transform>
|
||||||
|
> {
|
||||||
server?: boolean
|
server?: boolean
|
||||||
defer?: boolean
|
defer?: boolean
|
||||||
default?: () => T
|
default?: () => DataT
|
||||||
|
transform?: Transform
|
||||||
|
pick?: PickKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface _AsyncData<T> {
|
export interface _AsyncData<DataT> {
|
||||||
data: Ref<T>
|
data: Ref<DataT>
|
||||||
pending: Ref<boolean>
|
pending: Ref<boolean>
|
||||||
refresh: (force?: boolean) => Promise<void>
|
refresh: (force?: boolean) => Promise<void>
|
||||||
error?: any
|
error?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncData<T> = _AsyncData<T> & Promise<_AsyncData<T>>
|
export type AsyncData<Data> = _AsyncData<Data> & Promise<_AsyncData<Data>>
|
||||||
|
|
||||||
const getDefault = () => null
|
const getDefault = () => null
|
||||||
|
|
||||||
export function useAsyncData<T extends Record<string, any>> (key: string, handler: (ctx?: NuxtApp) => Promise<T>, options: AsyncDataOptions<T> = {}): AsyncData<T> {
|
export function useAsyncData<
|
||||||
|
DataT,
|
||||||
|
Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (
|
||||||
|
key: string,
|
||||||
|
handler: (ctx?: NuxtApp) => Promise<DataT>,
|
||||||
|
options: AsyncDataOptions<DataT, Transform, PickKeys> = {}
|
||||||
|
) : AsyncData<PickFrom<ReturnType<Transform>, PickKeys>> {
|
||||||
// Validate arguments
|
// Validate arguments
|
||||||
if (typeof key !== 'string') {
|
if (typeof key !== 'string') {
|
||||||
throw new TypeError('asyncData key must be a string')
|
throw new TypeError('asyncData key must be a string')
|
||||||
@ -51,7 +71,7 @@ export function useAsyncData<T extends Record<string, any>> (key: string, handle
|
|||||||
data: ref(nuxt.payload.data[key] ?? options.default()),
|
data: ref(nuxt.payload.data[key] ?? options.default()),
|
||||||
pending: ref(true),
|
pending: ref(true),
|
||||||
error: ref(null)
|
error: ref(null)
|
||||||
} as AsyncData<T>
|
} as AsyncData<DataT>
|
||||||
|
|
||||||
asyncData.refresh = (force?: boolean) => {
|
asyncData.refresh = (force?: boolean) => {
|
||||||
// Avoid fetching same key more than once at a time
|
// Avoid fetching same key more than once at a time
|
||||||
@ -63,6 +83,12 @@ export function useAsyncData<T extends Record<string, any>> (key: string, handle
|
|||||||
// TODO: Handle immediate errors
|
// TODO: Handle immediate errors
|
||||||
nuxt._asyncDataPromises[key] = Promise.resolve(handler(nuxt))
|
nuxt._asyncDataPromises[key] = Promise.resolve(handler(nuxt))
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
if (options.transform) {
|
||||||
|
result = options.transform(result)
|
||||||
|
}
|
||||||
|
if (options.pick) {
|
||||||
|
result = pick(result, options.pick) as DataT
|
||||||
|
}
|
||||||
asyncData.data.value = result
|
asyncData.data.value = result
|
||||||
asyncData.error.value = null
|
asyncData.error.value = null
|
||||||
})
|
})
|
||||||
@ -108,8 +134,17 @@ export function useAsyncData<T extends Record<string, any>> (key: string, handle
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allow directly awaiting on asyncData
|
// Allow directly awaiting on asyncData
|
||||||
const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData<T>
|
const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData<DataT>
|
||||||
Object.assign(asyncDataPromise, asyncData)
|
Object.assign(asyncDataPromise, asyncData)
|
||||||
|
|
||||||
return asyncDataPromise as AsyncData<T>
|
// @ts-ignore
|
||||||
|
return asyncDataPromise as AsyncData<DataT>
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick (obj: Record<string, any>, keys: string[]) {
|
||||||
|
const newObj = {}
|
||||||
|
for (const key of keys) {
|
||||||
|
newObj[key] = obj[key]
|
||||||
|
}
|
||||||
|
return newObj
|
||||||
}
|
}
|
||||||
|
44
packages/nuxt3/src/app/composables/fetch.ts
Normal file
44
packages/nuxt3/src/app/composables/fetch.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import type { FetchOptions } from 'ohmyfetch'
|
||||||
|
import type { $Fetch } from '@nuxt/nitro'
|
||||||
|
import type { AsyncDataOptions, _Transform, KeyOfRes } from './asyncData'
|
||||||
|
import { useAsyncData } from './asyncData'
|
||||||
|
|
||||||
|
export type Awaited<T> = T extends Promise<infer U> ? U : T
|
||||||
|
export type FetchResult<ReqT extends string> = Awaited<ReturnType<$Fetch<unknown, ReqT>>>
|
||||||
|
|
||||||
|
export type UseFetchOptions<
|
||||||
|
DataT,
|
||||||
|
Transform extends _Transform<DataT, any> = _Transform<DataT, DataT>,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> = AsyncDataOptions<DataT, Transform, PickKeys> & FetchOptions & { key?: string }
|
||||||
|
|
||||||
|
export function useFetch<
|
||||||
|
ReqT extends string = string,
|
||||||
|
ResT = FetchResult<ReqT>,
|
||||||
|
Transform extends (res: ResT) => any = (res: ResT) => ResT,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (
|
||||||
|
url: ReqT,
|
||||||
|
opts: UseFetchOptions<ResT, Transform, PickKeys> = {}
|
||||||
|
) {
|
||||||
|
if (!opts.key) {
|
||||||
|
const keys: any = { u: url }
|
||||||
|
if (opts.baseURL) {
|
||||||
|
keys.b = opts.baseURL
|
||||||
|
}
|
||||||
|
if (opts.method && opts.method.toLowerCase() !== 'get') {
|
||||||
|
keys.m = opts.method.toLowerCase()
|
||||||
|
}
|
||||||
|
if (opts.params) {
|
||||||
|
keys.p = opts.params
|
||||||
|
}
|
||||||
|
opts.key = generateKey(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
return useAsyncData('$f' + opts.key, () => $fetch(url, opts) as Promise<ResT>, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: More predictable universal hash
|
||||||
|
function generateKey (keys) {
|
||||||
|
return JSON.stringify(keys).replace(/[{":}=/,]|https?:\/\//g, '_').replace(/_+/g, '_')
|
||||||
|
}
|
@ -2,3 +2,4 @@ export { defineNuxtComponent } from './component'
|
|||||||
export { useAsyncData } from './asyncData'
|
export { useAsyncData } from './asyncData'
|
||||||
export { useHydration } from './hydrate'
|
export { useHydration } from './hydrate'
|
||||||
export { useState } from './state'
|
export { useState } from './state'
|
||||||
|
export { useFetch } from './fetch'
|
||||||
|
@ -5,7 +5,8 @@ const identifiers = {
|
|||||||
'useNuxtApp',
|
'useNuxtApp',
|
||||||
'defineNuxtPlugin',
|
'defineNuxtPlugin',
|
||||||
'useRuntimeConfig',
|
'useRuntimeConfig',
|
||||||
'useState'
|
'useState',
|
||||||
|
'useFetch'
|
||||||
],
|
],
|
||||||
'#meta': [
|
'#meta': [
|
||||||
'useMeta'
|
'useMeta'
|
||||||
|
@ -9230,6 +9230,14 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"example-use-fetch@workspace:examples/use-fetch":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "example-use-fetch@workspace:examples/use-fetch"
|
||||||
|
dependencies:
|
||||||
|
nuxt3: latest
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"example-use-state@workspace:examples/use-state":
|
"example-use-state@workspace:examples/use-state":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "example-use-state@workspace:examples/use-state"
|
resolution: "example-use-state@workspace:examples/use-state"
|
||||||
|
Loading…
Reference in New Issue
Block a user