In this lesson on sharing state between composables, in our course entitled: Vue 3 Composition API we did the unthinkable! We made a mistake 😱. That said, I can’t count how many times ChatGPT has hallucinated and given bad advice. Sorry I had too… 🤪
In this article though, we want to fix our mistake and use it as a valuable teaching opportunity in the process.
So first of all what is the issue? Well… we fell victim to one of the oldest gotcha’s in the book: a classic race condition. What is a race condition? According to Wikipedia:
A race condition or race hazard is the condition of an electronics, software, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events, leading to unexpected or inconsistent results. It becomes a bug when one or more of the possible behaviors is undesirable.
During the lesson, we created a composable that looks like this:
import { reactive, computed } from "vue";
const activeRequests = reactive([]);
export default function usePageRequests() {
const isLoading = computed(() => !!activeRequests.length);
const makeRequest = async (url) => {
// push the url to the activeRequests
const index = activeRequests.length;
activeRequests[index] = url;
const response = await fetch(url).catch((error) => alert(error)); // if failed remove the url from the activeRequests and alert error
const data = await response.json();
// remove the url from activeRequests
activeRequests.splice(index, 1);
return data;
};
return { isLoading, makeRequest };
}
The composable’s job is to make API requests and keep up with how many of them are actively in progress. The use case, was to show a single top level loading indicator while multiple nested components make separate requests. The top level loading indicator should only disappear once ALL requests were completed. Sure this could also be done with suspense, but the point of the lesson was to teach how to share reactive state (activeRequests
) amongst multiple calls to the usePageRequests
composable.
Everything worked just fine in the lesson so what’s the problem? The problem is that we are relying on the INDEX of the requests within the array to identify the request that should be removed once it completes.
// See here
const index = activeRequests.length;
activeRequests[index] = url;
// and here
activeRequests.splice(index, 1);
What’s wrong with this? The problem is that the index changes when other items are removed from the array! Therefore our code only works if the requests are completed in a reverse order (which must of happened while recording the video 🙂).
See how this fits with our definition of a race condition. Our system’s correct behavior was dependent on the timing of an uncontrollable event (the response time of the fetch request, which is totally out of the Vue apps control due to server availability, speed, network conditions, etc).
Let’s illustrate the problem this way. Let’s say we have a request called a
that we make first. And a request called b
that we make second.
While both requests are in progress the activeRequests
array looks like this ['a', 'b']
.
If b
finishes first then all is ok because it’s index
variable is set to 1
and it actually does exist at that position in the array so it’s properly removed (meaning then a
works as well at index 0
).
BUT what if a
finishes first! Then we have a problem! Why? Because once it’s removed b
shifts to the first position of the array (or index 0
) but the index
variable within the usePageRequests
function still says it’s at index 1
. So when it tries to remove it again, it can’t! Request b
gets stuck in the activeRequests
array and as far as our system knows all the requests are never complete. That sucks right? It means our loading indicator never disappears (and probably we never show the content to the users)
So what is the solution? We need to register our requests in the activeRequests
variable in such a way that removing one does not affect the identifier of the other. The best way I know of to do this is with a native JavaScript Map
and the Symbol
function. Thus our updated code would look like this:
import { reactive, computed } from "vue";
// 👇 We use map here
const activeRequests = reactive(new Map());
export default function usePageRequests() {
// 👇 We change is loading to look at the size property of the map
// instead of the length of the array
const isLoading = computed(() => activeRequests.size !== 0);
const makeRequest = async (url) => {
// 👇 here we set the request on the map
// using the symbol as the identifier
// instead of the index as the identifier on the array
const id = Symbol();
activeRequests.set(id, url); //
const response = await fetch(url).catch((error) => alert(error)); // if failed remove the url from the activeRequests and alert error
const data = await response.json();
// 👇 finally we remove the request from the map via .delete
// instead of splicing the array (at a position that may no longer be valid)
activeRequests.delete(id);
return data;
};
return { isLoading, makeRequest };
}
So there you have it! That’s how you would fix a race condition in a Vue.js composable. Have you ever made this mistake before? Are you interested in learning how to avoid other common mistakes in Vue.js? Checkout our 100% FREE course Common Vue.js Mistakes and How to Avoid Them.
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.