Say we have a component that receives the ID of a resource through a prop, resourceId. The component should fetch the corresponding resource from an API and display it, while also handling loading/error states (following a pattern similar to this one).
The function that fetches the resource from the external API also returns an abort function which, if called, causes the request to immediately reject. When resourceId changes, any in-flight requests should be aborted in favor of a new request. For what it's worth, I'm using fetch() and AbortController for this.
Using Vue 3 with the composition API, I came up with an implementation that looks something like this:
const loading = ref(false);
const data = ref(null);
const error = ref(null);
watchEffect(async (onCancel) => {
loading.value = true;
data.value = error.value = null;
const { response, abort } = fetchResourceFromApi(props.resourceId);
onCancel(abort);
try {
data.value = await (await response).json();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
});
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>{{ data }}</div>
This works fine under most circumstances, but breaks whenever the cancellation comes in to play. If resourceId changes before the last API request has finished, the following order of events happens:
abort()gets calledwatchEffectcallback gets called, settingloading,error, anddatacatchandfinallyblocks from original request are called, settingloadinganderror- Second API request completes and sets
loadinganddata
This results in an unexpected state where loading is set to false while the second request is in flight, error contains the exception raised by aborting the first request, and data contains the value from the second request.
Are there any design patterns or workarounds that can deal with this problem?