feat(nuxt3): `useFetch` (#721)

This commit is contained in:
pooya parsa 2021-10-12 00:36:50 +02:00 committed by GitHub
parent ad821d1aba
commit 54c57e3987
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 182 additions and 22 deletions

View File

@ -1,10 +1,10 @@
# 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`
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
@ -17,12 +17,19 @@ useAsyncData(key: string, fn: () => Object, options?: { defer: boolean, server:
* **options**:
- _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`)
- _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.
### Example
```vue
```js [server/api/index.ts]
let counter = 0
export default () => ++counter
```
```vue [pages/index.vue]
<script setup>
const { data } = await useAsyncData('time', () => $fetch('/api/count'))
</script>
@ -32,6 +39,42 @@ const { data } = await useAsyncData('time', () => $fetch('/api/count'))
</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
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
<script setup>
const { data: mountain } = await useAsyncData('everest', () =>
$fetch('/api/mountains/everest').then(({ title, description }) => ({ title, description }))
)
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
</script>
<template>

View File

@ -0,0 +1,4 @@
import { defineNuxtConfig } from 'nuxt3'
export default defineNuxtConfig({
})

View 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"
}
}

View File

@ -0,0 +1,9 @@
<template>
<div>
Server response: {{ data }}
</div>
</template>
<script setup>
const { data } = await useFetch('/api/hello', { params: { foo: 'bar' } })
</script>

View File

@ -0,0 +1,3 @@
import { useQuery } from 'h3'
export default req => ({ query: useQuery(req) })

View File

@ -4,7 +4,8 @@ import autoImports from '../../nuxt3/src/auto-imports/module'
// TODO: implement these: https://github.com/nuxt/framework/issues/549
const disabled = [
'useAsyncData',
'asyncData'
'asyncData',
'useFetch'
]
const identifiers = {

View File

@ -55,7 +55,6 @@ export async function build (nitroContext: NitroContext) {
nitroContext.rollupConfig = getRollupConfig(nitroContext)
await nitroContext._internal.hooks.callHook('nitro:rollup:before', nitroContext)
await writeTypes(nitroContext)
return nitroContext._nuxt.dev ? _watch(nitroContext) : _build(nitroContext)
}
@ -89,6 +88,7 @@ async function writeTypes (nitroContext: NitroContext) {
async function _build (nitroContext: NitroContext) {
nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir)
await writeTypes(nitroContext)
consola.start('Building server...')
const build = await rollup(nitroContext.rollupConfig).catch((error) => {
@ -151,4 +151,5 @@ async function _watch (nitroContext: NitroContext) {
}
}
)
await writeTypes(nitroContext)
}

View File

@ -23,9 +23,9 @@ export declare type TypedInternalResponse<Route, Default> =
: MiddlewareOf<Route>
: Default
export declare interface $Fetch {
<T = unknown, R extends FetchRequest = FetchRequest> (request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
raw<T = unknown, R extends FetchRequest = FetchRequest> (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
export declare interface $Fetch<T = unknown, R extends FetchRequest = FetchRequest> {
(request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
raw (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
}
declare global {

View File

@ -2,24 +2,44 @@ import { onBeforeMount, onUnmounted, ref } from 'vue'
import type { Ref } from 'vue'
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
defer?: boolean
default?: () => T
default?: () => DataT
transform?: Transform
pick?: PickKeys
}
export interface _AsyncData<T> {
data: Ref<T>
export interface _AsyncData<DataT> {
data: Ref<DataT>
pending: Ref<boolean>
refresh: (force?: boolean) => Promise<void>
error?: any
}
export type AsyncData<T> = _AsyncData<T> & Promise<_AsyncData<T>>
export type AsyncData<Data> = _AsyncData<Data> & Promise<_AsyncData<Data>>
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
if (typeof key !== '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()),
pending: ref(true),
error: ref(null)
} as AsyncData<T>
} as AsyncData<DataT>
asyncData.refresh = (force?: boolean) => {
// 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
nuxt._asyncDataPromises[key] = Promise.resolve(handler(nuxt))
.then((result) => {
if (options.transform) {
result = options.transform(result)
}
if (options.pick) {
result = pick(result, options.pick) as DataT
}
asyncData.data.value = result
asyncData.error.value = null
})
@ -108,8 +134,17 @@ export function useAsyncData<T extends Record<string, any>> (key: string, handle
}
// 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)
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
}

View 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, '_')
}

View File

@ -2,3 +2,4 @@ export { defineNuxtComponent } from './component'
export { useAsyncData } from './asyncData'
export { useHydration } from './hydrate'
export { useState } from './state'
export { useFetch } from './fetch'

View File

@ -5,7 +5,8 @@ const identifiers = {
'useNuxtApp',
'defineNuxtPlugin',
'useRuntimeConfig',
'useState'
'useState',
'useFetch'
],
'#meta': [
'useMeta'

View File

@ -9230,6 +9230,14 @@ __metadata:
languageName: unknown
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":
version: 0.0.0-use.local
resolution: "example-use-state@workspace:examples/use-state"