-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: port upstream useFetch, useLazyFetch
- Loading branch information
Showing
5 changed files
with
246 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,68 +1,248 @@ | ||
import type { FetchOptions, FetchRequest } from 'ofetch' | ||
import type { TypedInternalResponse } from 'nitropack' | ||
import type { FetchOptions, FetchError } from 'ofetch' | ||
import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack' | ||
import { hash } from 'ohash' | ||
import { computed, isRef } from 'vue' | ||
import { computed, unref, reactive } from 'vue' | ||
import type { Ref } from 'vue' | ||
import type { AsyncDataOptions, _Transform, KeyOfRes } from './asyncData' | ||
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData' | ||
import { useAsyncData } from './asyncData' | ||
import { useRequestFetch } from './ssr' | ||
|
||
export type FetchResult<ReqT extends FetchRequest> = TypedInternalResponse<ReqT, unknown> | ||
// support uppercase methods, detail: https://github.com/nuxt/nuxt/issues/22313 | ||
type AvailableRouterMethod<R extends NitroFetchRequest> = _AvailableRouterMethod<R> | Uppercase<_AvailableRouterMethod<R>> | ||
|
||
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, Lowercase<M>> | ||
|
||
type ComputedOptions<T extends Record<string, any>> = { | ||
[K in keyof T]: T[K] extends Function ? T[K] : ComputedOptions<T[K]> | Ref<T[K]> | T[K] | ||
} | ||
|
||
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> extends FetchOptions { | ||
method?: M; | ||
} | ||
|
||
type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R>> = ComputedOptions<NitroFetchOptions<R, M>> | ||
|
||
export interface UseFetchOptions< | ||
DataT, | ||
Transform extends _Transform<DataT, any> = _Transform<DataT, DataT>, | ||
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform> | ||
> extends | ||
AsyncDataOptions<DataT, Transform, PickKeys>, FetchOptions { key?: string } | ||
ResT, | ||
DataT = ResT, | ||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, | ||
DefaultT = null, | ||
R extends NitroFetchRequest = string & {}, | ||
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R> | ||
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> { | ||
key?: string | ||
$fetch?: typeof globalThis.$fetch | ||
watch?: MultiWatchSources | false | ||
} | ||
|
||
export function useFetch< | ||
ResT = void, | ||
ReqT extends FetchRequest = FetchRequest, | ||
_ResT = ResT extends void ? FetchResult<ReqT> : ResT, | ||
Transform extends (res: _ResT) => any = (res: _ResT) => _ResT, | ||
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform> | ||
ErrorT = FetchError, | ||
ReqT extends NitroFetchRequest = NitroFetchRequest, | ||
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>, | ||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT, | ||
DataT = _ResT, | ||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, | ||
DefaultT = null, | ||
> ( | ||
request: Ref<ReqT> | ReqT | (() => ReqT), | ||
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> | ||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null> | ||
export function useFetch< | ||
ResT = void, | ||
ErrorT = FetchError, | ||
ReqT extends NitroFetchRequest = NitroFetchRequest, | ||
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>, | ||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT, | ||
DataT = _ResT, | ||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, | ||
DefaultT = DataT, | ||
> ( | ||
request: Ref<ReqT> | ReqT | (() => ReqT), | ||
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> | ||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null> | ||
export function useFetch< | ||
ResT = void, | ||
ErrorT = FetchError, | ||
ReqT extends NitroFetchRequest = NitroFetchRequest, | ||
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>, | ||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT, | ||
DataT = _ResT, | ||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, | ||
DefaultT = null, | ||
> ( | ||
request: Ref<ReqT> | ReqT | (() => ReqT), | ||
opts: UseFetchOptions<_ResT, Transform, PickKeys> = {} | ||
arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, | ||
arg2?: string | ||
) { | ||
const key = '$f_' + (opts.key || hash([request, opts])) | ||
const _request = computed<FetchRequest>(() => { | ||
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] | ||
|
||
const _request = computed(() => { | ||
let r = request | ||
if (typeof r === 'function') { | ||
r = r() | ||
} | ||
return isRef(r) ? r.value : r | ||
return toValue(r) | ||
}) | ||
|
||
const _fetchOptions = { | ||
...opts, | ||
cache: typeof opts.cache === 'boolean' ? undefined : opts.cache | ||
const _key = opts.key || hash([autoKey, typeof _request.value === 'string' ? _request.value : '', ...generateOptionSegments(opts)]) | ||
if (!_key || typeof _key !== 'string') { | ||
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key) | ||
} | ||
if (!request) { | ||
throw new Error('[nuxt] [useFetch] request is missing.') | ||
} | ||
|
||
const _asyncDataOptions: AsyncDataOptions<_ResT, Transform, PickKeys> = { | ||
...opts, | ||
watch: [ | ||
_request, | ||
...(opts.watch || []) | ||
] | ||
const key = _key === autoKey ? '$f' + _key : _key | ||
|
||
if (!opts.baseURL && typeof _request.value === 'string' && (_request.value[0] === '/' && _request.value[1] === '/')) { | ||
throw new Error('[nuxt] [useFetch] the request URL must not start with "//".') | ||
} | ||
|
||
const asyncData = useAsyncData(key, () => { | ||
return $fetch(_request.value, _fetchOptions) as Promise<_ResT> | ||
const { | ||
server, | ||
lazy, | ||
default: defaultFn, | ||
transform, | ||
pick, | ||
watch, | ||
immediate, | ||
getCachedData, | ||
deep, | ||
dedupe, | ||
...fetchOptions | ||
} = opts | ||
|
||
const _fetchOptions = reactive({ | ||
...fetchOptions, | ||
cache: typeof opts.cache === 'boolean' ? undefined : opts.cache | ||
}) | ||
|
||
const _asyncDataOptions: AsyncDataOptions<_ResT, DataT, PickKeys, DefaultT> = { | ||
server, | ||
lazy, | ||
default: defaultFn, | ||
transform, | ||
pick, | ||
immediate, | ||
getCachedData, | ||
deep, | ||
dedupe, | ||
watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])] | ||
} | ||
|
||
let controller: AbortController | ||
|
||
const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => { | ||
controller?.abort?.() | ||
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController | ||
|
||
/** | ||
* Workaround for `timeout` not working due to custom abort controller | ||
* TODO: remove this when upstream issue is resolved | ||
* @see https://github.com/unjs/ofetch/issues/326 | ||
* @see https://github.com/unjs/ofetch/blob/bb2d72baa5d3f332a2185c20fc04e35d2c3e258d/src/fetch.ts#L152 | ||
*/ | ||
const timeoutLength = toValue(opts.timeout) | ||
if (timeoutLength) { | ||
setTimeout(() => controller.abort(), timeoutLength) | ||
} | ||
|
||
let _$fetch = opts.$fetch || globalThis.$fetch | ||
|
||
// Use fetch with request context and headers for server direct API calls | ||
if (process.server && !opts.$fetch) { | ||
const isLocalFetch = typeof _request.value === 'string' && _request.value[0] === '/' && (!toValue(opts.baseURL) || toValue(opts.baseURL)![0] === '/') | ||
if (isLocalFetch) { | ||
_$fetch = useRequestFetch() | ||
} | ||
} | ||
|
||
return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any) as Promise<_ResT> | ||
}, _asyncDataOptions) | ||
|
||
return asyncData | ||
} | ||
|
||
/** | ||
* Fetch data from an API endpoint with an SSR-friendly composable. | ||
* See {@link https://nuxt.com/docs/api/composables/use-fetch} | ||
* @param request The URL to fetch | ||
* @param opts extends $fetch options and useAsyncData options | ||
*/ | ||
export function useLazyFetch< | ||
ResT = void, | ||
ErrorT = FetchError, | ||
ReqT extends NitroFetchRequest = NitroFetchRequest, | ||
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>, | ||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT, | ||
DataT = _ResT, | ||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, | ||
DefaultT = null, | ||
> ( | ||
request: Ref<ReqT> | ReqT | (() => ReqT), | ||
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'> | ||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null> | ||
export function useLazyFetch< | ||
ResT = void, | ||
ReqT extends string = string, | ||
_ResT = ResT extends void ? FetchResult<ReqT> : ResT, | ||
Transform extends (res: _ResT) => any = (res: _ResT) => _ResT, | ||
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform> | ||
ErrorT = FetchError, | ||
ReqT extends NitroFetchRequest = NitroFetchRequest, | ||
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>, | ||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT, | ||
DataT = _ResT, | ||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, | ||
DefaultT = DataT, | ||
> ( | ||
request: Ref<ReqT> | ReqT | (() => ReqT), | ||
opts: Omit<UseFetchOptions<_ResT, Transform, PickKeys>, 'lazy'> = {} | ||
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'> | ||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null> | ||
export function useLazyFetch< | ||
ResT = void, | ||
ErrorT = FetchError, | ||
ReqT extends NitroFetchRequest = NitroFetchRequest, | ||
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>, | ||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT, | ||
DataT = _ResT, | ||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, | ||
DefaultT = null, | ||
> ( | ||
request: Ref<ReqT> | ReqT | (() => ReqT), | ||
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>, | ||
arg2?: string | ||
) { | ||
return useFetch(request, { ...opts, lazy: true }) | ||
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] | ||
return useFetch<ResT, ErrorT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>(request, { | ||
...opts, | ||
lazy: true | ||
}, | ||
// @ts-expect-error we pass an extra argument with the resolved auto-key to prevent another from being injected | ||
autoKey) | ||
} | ||
|
||
type MaybeRef<T> = T | Ref<T> | ||
type MaybeRefOrGetter<T> = MaybeRef<T> | (() => T) | ||
type AnyFn = (...args: any[]) => any | ||
|
||
function toValue<T> (r: MaybeRefOrGetter<T>): T { | ||
return typeof r === 'function' | ||
? (r as AnyFn)() | ||
: unref(r) | ||
} | ||
|
||
function generateOptionSegments <_ResT, DataT, DefaultT> (opts: UseFetchOptions<_ResT, DataT, any, DefaultT, any, any>) { | ||
const segments: Array<string | undefined | Record<string, string>> = [ | ||
toValue(opts.method as MaybeRef<string | undefined> | undefined)?.toUpperCase() || 'GET', | ||
toValue(opts.baseURL) | ||
] | ||
for (const _obj of [opts.params || opts.query]) { | ||
const obj = toValue(_obj) | ||
if (!obj) { continue } | ||
|
||
const unwrapped: Record<string, string> = {} | ||
for (const [key, value] of Object.entries(obj)) { | ||
unwrapped[toValue(key)] = toValue(value) | ||
} | ||
segments.push(unwrapped) | ||
} | ||
return segments | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<script setup> | ||
const name = ref('') | ||
const query = computed(() => ({ name: name.value })) | ||
const { data, refresh } = useLazyFetch('/api/hello', { query, watch: false }) | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<h1>Lazy Fetch</h1> | ||
<input v-model="name"> | ||
<button @click="() => refresh()"> | ||
Refresh | ||
</button> | ||
<div>{{ data }}</div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters