Skip to content

Instantly share code, notes, and snippets.

@puppy0cam
Created February 15, 2023 03:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save puppy0cam/3e2a805d3b4a57c4fbf1d0496e50aac9 to your computer and use it in GitHub Desktop.
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
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;
}
<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