Imagine a scenario where a component receives the ID of a resource through a prop called resourceId
. This component is responsible for fetching the corresponding resource from an API and displaying it, while also managing loading and error states (similar to the pattern outlined here).
The function that retrieves the resource from the external API includes an abort
function which, when invoked, immediately rejects the request. If the value of resourceId
changes during an ongoing request, the existing request should be aborted in favor of initiating a new one. Just to provide some context, this implementation involves using fetch()
and AbortController
.
Utilizing Vue 3 with the composition API, I developed an implementation that resembles the following:
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>
Although this approach generally works well, issues arise when dealing with cancellations. If a change in resourceId
occurs before the completion of the last API request, the following sequence of events takes place:
abort()
is triggered- The
watchEffect
callback is executed, setting values forloading
,error
, anddata
- The
catch
andfinally
blocks from the initial request run, affectingloading
anderror
- The second API request finishes and updates
loading
anddata
This leads to an unexpected state where loading
is set to false
while the second request is still ongoing, error
contains the exception raised by aborting the first request, and data
holds the value from the second request.
Are there any recommended design patterns or solutions to address this issue?