Skip to content

Instantly share code, notes, and snippets.

@tq-bit
Last active November 22, 2023 11:54
Show Gist options
  • Save tq-bit/79d6ab61727ebf29ed0ff9ddc4deedca to your computer and use it in GitHub Desktop.
Save tq-bit/79d6ab61727ebf29ed0ff9ddc4deedca to your computer and use it in GitHub Desktop.
A Vue 3 Compostion Hook (MVP) for the Fetch API. Its public methods can be used to monitor and cancel a fetch request.
<script setup>
import { ref, onMounted } from 'vue';
import AppForm from './components/AppForm.vue';
import AppList from './components/AppList.vue';
import AppLoading from './components/AppLoading.vue';
import useFetch from './use/useFetch';
const rootUrl = 'https://my-json-server.typicode.com/tq-bit/use-fetch-json-mock';
const { loading, results, getJson } = useFetch(rootUrl);
onMounted(() => getPosts());
function getPosts() {
return getJson('/posts')
}
</script>
<template>
<app-form></app-form>
<app-loading v-if="loading"></app-loading>
<app-list v-else :items="results"></app-list>
</template>
import { ref } from 'vue';
export default function useFetch(rootUrl) {
let loading = ref(false);
let progress = ref(0);
let chunks = ref([]);
let results = ref({});
let errors = ref([]);
let controller = null;
async function getJson(path, options) {
return _fetch('get', path, options);
}
async function postJson(path, payload, options = {}) {
options.body = JSON.stringify(payload);
return _fetch('post', path, options);
}
async function _fetch(method, path, options) {
_resetLocals();
controller = new AbortController();
const signal = controller.signal;
loading.value = true;
try {
results.value = await fetchData();
} catch (error) {
errors.value.push(error);
} finally {
loading.value = false;
}
async function fetchData() {
const response = await fetch(`${rootUrl}${path}`, { method, signal, ...options });
const { status } = response;
if (status >= 200 && status < 300) {
return JSON.parse(await _readBody(response));
} else {
throw new Error(await _readBody(response));
}
}
}
async function _readBody(response, encoding = 'utf-8') {
const reader = response.body.getReader();
const contentLength = _getContentLength();
let received = ref(0);
// Loop through the response stream and extract data chunks
while (loading.value === true) {
const { done, value: dataChunk } = await reader.read();
if (done) {
_finishLoading();
} else {
_handleLoadingProgress(dataChunk);
}
}
const body = _assembleResponseBody(received.value);
// Decode the response and return it
return new TextDecoder(encoding).decode(body);
function _getContentLength() {
const hasContentLengthHeaders = response.headers.has('content-length');
if (!hasContentLengthHeaders) {
console.warn(
'Cannot calculate serverside payload size. To use the progress indicator, you must configure the "content-length" header on your serverside'
);
}
return +response.headers.get('content-length');
}
function _finishLoading() {
loading.value = false;
}
function _handleLoadingProgress(dataChunk) {
chunks.value.push(dataChunk);
received.value += dataChunk.length;
progress.value = (received.value * 100) / contentLength;
}
function _assembleResponseBody(received) {
let body = new Uint8Array(received);
let position = 0;
// Order the chunks by their respective position
for (let chunk of chunks.value) {
body.set(chunk, position);
position += chunk.length;
}
return body;
}
}
function _resetLocals() {
loading.value = false;
progress.value = 0;
chunks.value = [];
results.value = {};
errors.value = [];
controller = null;
}
function cancelRequest() {
if (!controller) {
throw new Error('Cannot cancel request - no AbortController was assigned');
}
controller.abort();
return _resetLocals();
}
return { loading, progress, results, errors, cancelRequest, getJson, postJson };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment