Created
February 15, 2023 03:57
-
-
Save puppy0cam/3e2a805d3b4a57c4fbf1d0496e50aac9 to your computer and use it in GitHub Desktop.
Helpful function for Vue 3 that allows you to achieve asynchronous computed values without making the entire component async
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
import { onBeforeUnmount, shallowRef, watchEffect, type ComponentInternalInstance, type ShallowRef } from "vue"; | |
/** | |
* Creates a computed value that is bound to the lifecycle of the component. | |
* Within the callback, all reactive values that need to be tracked **must** be called before any kind of await operation. | |
* | |
* @example ```typescript | |
* const purchaseorder: Readonly<ShallowRef<purchaseorder | null>> = createAsyncComponentComputedValue(() => { | |
* const purchaseorder_id = props.purchaseorder_id; | |
* return purchaseorder__query_singular_required({ | |
* purchaseorder_id, | |
* project_id: project.value.project_id, | |
* }); | |
* }, null); | |
* ``` | |
*/ | |
export function createAsyncComponentComputedValue<T, DefaultValue extends Awaited<T> | null = Awaited<T> | null>(callback: (onCancel: (callback: () => void) => void) => T | Promise<Awaited<T>>, defaultValue: DefaultValue = null as any, instance?: ComponentInternalInstance): Readonly<ShallowRef<Awaited<T> | DefaultValue>> { | |
const res = shallowRef<Awaited<T> | null | DefaultValue>(defaultValue); // the value to handle. | |
onBeforeUnmount(watchEffect((onCleanup) => { // watch the effects and clean up when the component is unmounted. | |
let uncancelled = true; // whether the callback has been cancelled. | |
const cancel_listeners: (() => void)[] = []; | |
onCleanup(() => { // when the component is unmounted, cancel the callback. | |
uncancelled = false; | |
for (let i = 0; i < cancel_listeners.length; i++) { | |
cancel_listeners[i](); | |
} | |
}); | |
try { // create a try block as we want to know if an error occurs. | |
const value = callback((onCancel) => { | |
cancel_listeners.push(onCancel); | |
}); // call the callback function. | |
if (value instanceof Promise) { // if the value is a promise, then we need to await it. | |
if (uncancelled) { // if this execution has been cancelled, then we don't need to do anything anymore. | |
// it's not ok to allow the old state to be as is, as we don't know if something else relies on the value. | |
// For example, if we were watching the currently active project, we want to immediately discard data from the old project | |
// because issues *will* occur from cross-project data mixing. | |
// this is why we set the value to the default value. | |
// So that as far as the component is concerned, the state has just been reset as if we had just mounted the component and are still waiting for the data. | |
res.value = defaultValue; | |
value.then((val) => { | |
if (uncancelled) { | |
res.value = val; | |
} | |
}); | |
} | |
} else { | |
if (uncancelled) { | |
res.value = value as Awaited<T>; | |
} | |
} | |
} catch (error) { | |
if (uncancelled) { | |
// if an error occurs, the current state should no longer be trusted. | |
// So we need to get to a state where the component thinks it's just been mounted and is waiting for data. | |
// This prevents us from having cross-contamination of unrelated data. | |
res.value = defaultValue; | |
} | |
throw error; | |
} | |
}), instance); | |
return res as any; | |
} |
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
<template> | |
<img v-if="url" :src="url" /> | |
<img v-else src="./assets/placeholder.png" /> | |
</template> | |
<script lang="ts"> | |
import { defineComponent } from "vue"; | |
import { createAsyncComponentComputedValue } from "./createAsyncComponentComputedValue.js"; | |
export default defineComponent({ | |
props: { | |
file_id: { | |
type: Number, | |
required: true, | |
}, | |
}, | |
setup: (props) => { | |
const url = createAsyncComponentComputedValue(async (onCancel) => { | |
// using the onCancel callback is not required | |
// but it can be useful in some niche cases. | |
const canceller = new AbortController(); | |
onCancel(() => { | |
canceller.abort(); | |
}); | |
const response = await fetch(`/api/file/${props.file_id}`, { | |
signal: canceller.signal, | |
}); | |
if (!response.ok) { | |
throw new Error("Failed to fetch file"); | |
} | |
const info = await response.json(); | |
return info?.url as string | undefined; | |
}, null); | |
return { | |
url, | |
}; | |
}, | |
}); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment