Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useFetch): new function #330

Merged
merged 25 commits into from
Feb 23, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
47dec5c
feat(useWebSocket)!: rework useWebSocket
antfu Feb 9, 2021
0ffbe6c
feat(useFetch): initial implementation of useFetch
wheatjs Feb 12, 2021
38a9a94
updated signature overrides and documentation for UseFetchOptions
wheatjs Feb 12, 2021
f41c653
Renamed autoFetch to immediate
wheatjs Feb 12, 2021
954a037
Renamed autoRefetch to refetch
wheatjs Feb 12, 2021
9680574
Removed string from error message
wheatjs Feb 12, 2021
83b82ef
Updated demos and docs to reflect changes in API. Args now use length…
wheatjs Feb 12, 2021
8f9d850
Rename status to statusCode
wheatjs Feb 12, 2021
fc387cb
set statusCode to null on fetch
wheatjs Feb 12, 2021
b56d392
Reset varaibles on refetch and rename status to statusCode
wheatjs Feb 12, 2021
d692c28
Remove instanceof from Abort controller check
wheatjs Feb 12, 2021
0d96f10
Simplify refetch watch
wheatjs Feb 12, 2021
b9a8e2d
Patched watch bug for now. Started looking into apis for usePostJson
wheatjs Feb 14, 2021
237724c
chore: api design
antfu Feb 19, 2021
374ed94
fixed issue with new api implementation. Started working on tests
wheatjs Feb 19, 2021
cc8edad
Merge branch 'main' into main
antfu Feb 20, 2021
34e8fed
fixed issue with initialized check
wheatjs Feb 20, 2021
345d5de
Merge branch 'main' of github.com:jacobclevenger/vueuse into main
wheatjs Feb 20, 2021
cf21b90
Merge branch 'main' into main
antfu Feb 20, 2021
8530f98
chore: update docs
antfu Feb 20, 2021
ae739be
execute now returns a promise
wheatjs Feb 21, 2021
2c5092a
fixed overly verbose return in execute function
wheatjs Feb 21, 2021
acb7bf0
Added 'aborted' proprty to useFetch. Added in basic tests
wheatjs Feb 22, 2021
9e07337
Merge remote-tracking branch 'origin/main' into pr/jacobclevenger/330
antfu Feb 23, 2021
5ad7f98
chore: update locks
antfu Feb 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 64 additions & 0 deletions packages/core/useFetch/demo.vue
@@ -0,0 +1,64 @@
<script setup lang="ts">
import { reactive, ref } from 'vue-demi'
import { stringify } from '@vueuse/docs-utils'
import { useToggle } from '@vueuse/shared'
import { useFetch } from '.'

const url = ref('https://httpbin.org/get')
const refetch = ref(false)

const toggleRefetch = useToggle(refetch)

const { isFinished, canAbort, isFetching, status, error, data, execute, abort } = useFetch(url, { method: 'GET' }, { immediate: false, refetch })

const text = stringify(reactive({
isFinished,
isFetching,
canAbort,
status,
error,
data,
}))

</script>

<template>
<div>
<div>
<note>The following URLs can be used to test different features of useFetch</note>
<div class="mt-2">
Normal Request:
<code>
https://httpbin.org/get
</code>
</div>
<div>
Abort Request:
<code>
https://httpbin.org/delay/10
</code>
</div>
<div>
Response Error:
<code>
http://httpbin.org/status/500
</code>
</div>
</div>

<input v-model="url" type="text">
<button @click="execute">
Execute
</button>
<button @click="toggleRefetch">
<carbon-checkmark v-if="refetch" />
<carbon-error v-else />

<span class="ml-2">{{ refetch ? 'Refetch On': 'Refetch Off' }}</span>
</button>
<button v-if="canAbort" class="orange" @click="abort">
Abort
</button>
<pre lang="yaml">{{ text }}</pre>
</div>
</template>
133 changes: 133 additions & 0 deletions packages/core/useFetch/index.md
@@ -0,0 +1,133 @@
---
category: Browser
---

# useFetch

Reactive [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) with support for [aborting requests](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort), in browsers that support it.

## Usage

```ts
import { useFetch } from '@vueuse/core'

const { isFinished, status, error, data } = useFetch(url)
```

Prevent auto-calling the fetch request and do it manually instead

```ts
import { useFetch } from '@vueuse/core'

const { execute, data } = useFetch(url, { immediate: false })

execute()
```

Automatically refetch when your URL is a ref

```ts
import { useFetch } from '@vueuse/core'

const url = ref('https://httpbin.org/get')

const { data } = useFetch(url, { refetch: true })

setTimeout(() => {
// Request will be fetched again
url.value = 'https://httpbin.org/status/500'
}, 5000)
```

Using normal fetch request options

```ts
import { useFetch } from '@vueuse/core'

const { execute, data } = useFetch(url,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
{ immediate: false })


```

<!--FOOTER_STARTS-->
## Type Declarations

```typescript
interface UseFetchReturn {
/**
* Indicates if the fetch request has finished
*/
isFinished: Ref<boolean>
/**
* The status of the fetch request
*/
status: Ref<number | null>
/**
* The raw response of the fetch request
*/
response: Ref<Response | null>
/**
* Any fetch errors that may have occured
*/
error: Ref<any>
/**
* The fetch response body, may either be JSON or text
*/
data: Ref<object | string | null>
/**
* Indicates if the request is currently being fetched.
*/
isFetching: Ref<boolean>
/**
* Indicates if the fetch request is able to be aborted
*/
canAbort: ComputedRef<boolean>
/**
* Abort the fetch request
*/
abort: Fn
/**
* Manually call the fetch
*/
execute: Fn
}
interface UseFetchOptions {
/**
* Will automatically run fetch when `useFetch` is used
* Default: true
*/
autoFetch?: boolean
/**
* Will automatically refetch when the URL is changed if the url is a ref
* Default: false
*/
autoRefetch?: MaybeRef<boolean>
}
export declare function useFetch(url: MaybeRef<string>): UseFetchReturn
export declare function useFetch(
url: MaybeRef<string>,
useFetchOptions: UseFetchOptions
): UseFetchReturn
export declare function useFetch(
url: MaybeRef<string>,
options: RequestInit
): UseFetchReturn
export declare function useFetch(
url: MaybeRef<string>,
options: RequestInit,
useFetchOptions: UseFetchOptions
): UseFetchReturn
export {}
```

## Source

[Source](https://github.com/vueuse/vueuse/blob/main/packages/core/useFetch/index.ts) • [Demo](https://github.com/vueuse/vueuse/blob/main/packages/core/useFetch/demo.vue) • [Docs](https://github.com/vueuse/vueuse/blob/main/packages/core/useFetch/index.md)


<!--FOOTER_ENDS-->
161 changes: 161 additions & 0 deletions packages/core/useFetch/index.ts
@@ -0,0 +1,161 @@
import { Ref, ref, unref, watch, isRef, computed, ComputedRef } from 'vue-demi'
import { Fn, MaybeRef } from '@vueuse/shared'

interface UseFetchReturn {
/**
* Indicates if the fetch request has finished
*/
isFinished: Ref<boolean>

/**
* The statusCode of the HTTP fetch response
*/
status: Ref<number | null>
wheatjs marked this conversation as resolved.
Show resolved Hide resolved
wheatjs marked this conversation as resolved.
Show resolved Hide resolved

/**
* The raw response of the fetch response
*/
response: Ref<Response | null>

/**
* Any fetch errors that may have occured
*/
error: Ref<any>

/**
* The fetch response body, may either be JSON or text
*/
data: Ref<object | string | null>

/**
* Indicates if the request is currently being fetched.
*/
isFetching: Ref<boolean>

/**
* Indicates if the fetch request is able to be aborted
*/
canAbort: ComputedRef<boolean>

/**
* Abort the fetch request
*/
abort: Fn

/**
* Manually call the fetch
*/
execute: Fn
}

interface UseFetchOptions {
/**
* Will automatically run fetch when `useFetch` is used
*
* @default true
*/
immediate?: boolean

/**
* Will automatically refetch when the URL is changed if the url is a ref
*
* @default false
*/
refetch?: MaybeRef<boolean>
}

export function useFetch(url: MaybeRef<string>): UseFetchReturn
export function useFetch(url: MaybeRef<string>, useFetchOptions: UseFetchOptions): UseFetchReturn
export function useFetch(url: MaybeRef<string>, options: RequestInit, useFetchOptions?: UseFetchOptions): UseFetchReturn

export function useFetch(url: MaybeRef<string>, ...args: any[]): UseFetchReturn {
const supportsAbort = typeof AbortController === 'function'

let fetchOptions: RequestInit = {}
let options: UseFetchOptions = { immediate: true, refetch: false }

if (args.length > 0) {
if ('immediate' in args[0] || 'refetch' in args[0])
options = { ...options, ...args[0] }
else
fetchOptions = args[0]
}

if (args.length > 1) {
if ('immediate' in args[1] || 'refetch' in args[1])
options = { ...options, ...args[1] }
}

const isFinished = ref(false)
const isFetching = ref(false)
const status = ref<number | null>(null)
const response = ref<Response | null>(null)
const error = ref<any>(null)
const data = ref<string | object | null>(null)

const canAbort = computed(() => {
return supportsAbort && isFetching.value
})

let controller: AbortController | null
wheatjs marked this conversation as resolved.
Show resolved Hide resolved

const abort = () => {
if (supportsAbort && controller instanceof AbortController)
wheatjs marked this conversation as resolved.
Show resolved Hide resolved
controller.abort()
}

const execute = () => {
isFetching.value = true
isFinished.value = false
error.value = null
wheatjs marked this conversation as resolved.
Show resolved Hide resolved

if (supportsAbort) {
controller = new AbortController()
fetchOptions = { ...fetchOptions, signal: controller.signal }
}

fetch(unref(url), fetchOptions)
.then((fetchResponse) => {
response.value = fetchResponse
status.value = fetchResponse.status

const contentType = fetchResponse.headers.get('content-type')

if (contentType && contentType.includes('application/json'))
fetchResponse.json().then(json => data.value = json)
else
fetchResponse.text().then(text => data.value = text)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make this usage simpler with some API?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is there something out there that would allow us to simplify it? I'm not aware of anything, but if you know I'll update it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. But maybe keep your implementation and make a useFetchJson wrapper with preconfigured header? Moreover, something like usePost and usePostJson?

Another approach might be similar to ky

useFetch(URL, {}).json()

WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm doing more research into the fetch API they actually have more than just text() and json(), https://developer.mozilla.org/en-US/docs/Web/API/Body#methods

If we were to make a wrapper for all of these we would have useFetch, useFechJson, useFetchBlob, useFetchFormData, and useFetchArrayBuffer. But I feel like most of these would not be used very often and it would create quite a few methods. Perhaps another option would be to have an option in the config to set how the data is parsed. Something like this perhaps?

useFetch(url, {
  // Not sure what a good name for this would be
  // It could be defaulted toauto which would try to automatically detect the type from the response headers
  readAs: 'auto' //could also be json, formData, blob, arrayBuffer, or text
})

I do like the idea of wrappers for usePost and usePostJson. If we do that would we also want ones for form-data and multipart form data?


// see: https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
if (!fetchResponse.ok)
throw new Error(fetchResponse.statusText)
})
.catch(fetchError => error.value = fetchError.message)
.finally(() => {
isFinished.value = true
isFetching.value = false
})
}

if (isRef(url)) {
if (isRef(options.refetch))
watch([options.refetch, url], () => unref(options.refetch) && execute())
else if (options.refetch)
watch(url, () => execute())
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isRef(url)) {
if (isRef(options.refetch))
watch([options.refetch, url], () => unref(options.refetch) && execute())
else if (options.refetch)
watch(url, () => execute())
}
watch(
()=>{
unref(url)
unref(options.refetch)
},
() => unref(options.refetch) && execute()
)

Sorry for the formatting

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, I can fix it in a sec

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't actually seem to work. Can you watch an unref? I was under the impression it just gave you back the raw value if it was a ref

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test it? It's not watching an unref but the "accessing". unref is just a unified way to accessing here, the return value doesn't matter. Should work I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok it looks like it does work, but the watch needs to have deep: true set

Copy link
Member

@antfu antfu Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Em? I don't think it's necessary... 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, it only seems to work when I set deep: true. Not really sure why

Copy link
Member Author

@wheatjs wheatjs Feb 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just created a repo to demonstrate the issue. It is a bit of a contrived example, but it should show the issue. It is in vite so you should be able to just clone it and run it https://github.com/jacobclevenger/vue3-watch-bug/blob/master/src/App.vue


if (options.immediate)
execute()

return {
isFinished,
status,
response,
error,
data,
isFetching,
canAbort,
abort,
execute,
}
}