Fixing Race Conditions

Here we'll learn how to fix race conditions

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the React Data Fetching: Beyond the Basics course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to React Data Fetching: Beyond the Basics, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course React Data Fetching: Beyond the Basics
  • [00:00 - 00:45] Okay, now that we've added refetched functionality, we can go ahead and work on the issue with the race conditions. We're going to be using an app called the Star Wars Random Name Generator. This was originally created by Max Rosen, so I can't take any credit for it, but I've made a few tweaks to help us see the race conditions issue when it happens. Let 's quickly take a look at the code. If we open the Explorer, you can see this app contains four files, main and index CSS are pretty self-explanatory. The only files I'm going to go through are app and data display. Let's start with the app component. You can see here that online 8 is generating a random ID and setting it to the state using the setCurrentID function.

    [00:46 - 01:28] If we scroll down, you can see in the JSX online 15, there's a H1 for the name, and then the requesting ID, which is the ID that's been set in the state. Then, when the button clicks or when the user clicks a button, it runs the handleClick function, which creates a new random ID and sets that to the state. If we scroll down even further, you can see there's a component called DataTheS player online 22, which takes the current ID as an ID prop. Let's have a look at the DataDisplay component. Here you can see online 8 that there's a use effect hook, which is re-rendered each time the ID prop changes. Remember, the ID prop is coming directly from the app component.

    [01:29 - 02:50] When the component mounts and whenever the ID changes, this fetchData function will run, which first gets data from the styles API based on the ID and then sets the data to the state using the setData function. If we scroll down here, there's some JSX online 17, which runs only if data exists and if there's no data, it will return now. Let's wrap the text so we're not scrolling horizontally. We can see here that when the data gets fetched, the text will be green, and while it's being fetched, the text will be red. So you can imagine, when someone first clicks the button, the text will be red because the ID from the app component is not the same as the fetch ID from the styles API. Then, when both IDs match, the text will go green and the name of the person will be displayed down here. Now, there's something special about this specific API. Let's take a look at it. We can see that lines two, three, and four are creating a random number and then passing that number so we can see the time in seconds and then console logging it to help us see how long each request should take. And why is it doing this? Well, if we scroll down in the setTimeout, the random time is passed as an argument, meaning that we are artificially slowing down the length of each response based on the random time that's generated here.

    [02:51 - 03:22] And this is one of the best ways to illustrate the race condition issue because each call that is made will take a random amount of time to resolve. So if a call is made last but resolves the quickest, then it will not be shown. Let's demonstrate this by looking at the app in the browser. If we zoom in a bit and then right click, click on inspect and change the tab to network, we can see right now I've got cache disabled. Let's turn that on since that won 't make a difference for this demonstration and we can press escape to show the console.

    [03:23 - 03:46] So let's first press control R to refresh the page and we can see that it's waiting for about four seconds before it makes the call to one and then that's resolved. And the reason the call is made twice is because of the strict mode component. So let's press the fetch data button a few times, one, two, three, and we can see that each call will take a different amount of time to resolve. So 57 resolved quickest.

    [03:47 - 04:05] Next it's 14 and then it's 45. So even though the fetch data button was skipped on three times and the last result was 57, because 57 resolves the quickest, it's not shown. The result that shows is the request that took the longest to make, which is 45. Let's go back to the presentation.

    [04:06 - 04:43] Now the solution to the race conditions issue is first to add in the book controller, then pass that into the fetch so that when the react component amounts, that call is aborted. Remember, whenever we click on the button, this means the component will re render. So it will first amount and then mount a new component, which means each time we click, we can abort the fetch call. But this code we see here is a lot to write. And if we have a lot of components with this issue, it will take a long time to write all of this by hand. We can make the solution a bit simpler by incorporating it into our data loader code. Let's go ahead and do that.

    [04:44 - 05:05] First of all, let's update the data displayer component to use our custom hook instead of use effect. At the end of line five, hit enter and we're eventually going to delete line five, but let's create the replacement. Warwrights const, that on curly braces and inside the curly braces, we're going to write data comma refetch. Then we're going to run our use data function.

    [05:06 - 05:22] And if we open the file explorer, we can see we have a utils folder with our data loader file. So we can go ahead and import that at the end of line two, it's enter, right imports, add curly braces and inside the curly braces, we're going to write use data.

    [05:23 - 06:50] And that's going to be from our quotation marks and the right dot dot slash go to utils, and then open up our data loader file. Let's finish off line seven, we're going to add parentheses. And in the first argument, we'll give it a key of person name, then we'll give us an argument, which would be our feature function. And here we're going to add parentheses, then we'll make that an arrow function. And then we're going to write get data from ID, make sure data from an ID have capital letters, then add parentheses, and we'll pass in the ID as an argument. Now we can get rid of the code on line six. And we can delete all the code inside these effects, apart from the dependency array, and we'll replace it with our refetch function. Nice. This looks a lot cleaner than it did before. Now, because of the way the code works, we have to keep these effects hook. The ID from line five is being passed down as a prop. And we only want to refetch when the ID changes . Alternatively, we could have called the refetch function in our app component down here, when the random ID is generated. Or if we wanted to keep the logic inside the data, the spare component, we could have some logic that stores the previous ID value in the state, and compares that to the current ID, and only call the refetch if they're different. But the user effect hook was designed to handle this kind of logic. So for now, we'll stick to it. Cool. Let's go into our data load of file, and make some adjustments to the code that will fix the race conditions bug.

    [06:51 - 08:23] Let's roll down to our prefetch data function, online 16. And since this is where we do the fetch call, let's add the abort controller here. So at the end of line 16, hit enter, and writes const new controller, and we'll make that equal a new abort controller. Add parentheses and close with a semicolon. Now we'll press enter again, and we 'll get the signal out of the abort controller. And the signal is the thing that we can pass into the fetch call, so that it can be canceled by our abort controller. So we'll write const signal equals new controller, but signal. Now what we can do is get this signal variable, copy it and paste it inside the parentheses in the FM online 19. Let's make some space by pressing enter after line 18, and now this can be added to the fetch call. But we'll stay in this file for now before we go to the fetch call and add the signal below our controller code on line 17, we can write an if statement to check if the request has a status of loading, and if it does, then we can abort it. Let's go to the end of line 18, we'll hit enter a few times and write if our parentheses and inside the parentheses, we're going to write data cache dot get our parentheses. And here we're going to put key. So we're going to get the data from the cache that matches this key, then we'll get the status, and we'll make sure it has a status of loading, then we'll add some curly braces. So we'll write snue controller dot abort, then add parentheses and close with a semicolon.

    [08:24 - 09:26] Cool. Now there's actually a problem with this code we've just written. If we scroll up in this file and look at line seven, we can see that any new request that doesn't have a value in the data cache is given as status of loading. And also, if we scroll down to find our refetch function, which should be online 44, we can see here, online 45, that any function that is refetch will also get a status of loading, which means every time we do a new fetch call, or we refetch, those requests would immediately be aborted. What we could do to fix this is add a new status, something like empty or idle, which will be set when the data is first fetched, and when the data is refet ched. This means that loading can be used for when the data is actually loading. Let 's go ahead and make those changes. In our status response object, which is online 49, let's add any status of idle, and we'll give this the same value as loading. So we'll copy all this code and put it on round 50.

    [09:27 - 09:50] Next, we can change our refetch function. Let's think about how we can do that. If a request has a state of loading and the refetch function runs, we don't want to set that one to idle. We want to leave it as loading. Idle should only be set if the status is error or success, which means we should have data. With that in mind, let's go ahead and add an if statement to the refetch function.

    [09:51 - 10:58] Let's get to the end of line 44, hit enter, and we're going to write this, add parentheses, then write data, cache.get, add parentheses, and we'll put key in the parentheses, then we'll check the status and make sure it doesn't equal loading. So if it doesn't equal loading, then we can change the status to idle. But if it does, we want to leave it as loading. We'll add curly braces and we'll put the closing curly brace after line 46. Then online 46, where it has the word loading, we'll change that to idle. Now let's go up to the data function and we'll change line seven from status or from loading to idle. And then we can scroll down to our prefetch data function. So that would be on line 16, we can find our FN, which is on line 24. And above that, we can change the status. So here, we'll write data, cache. set. And we'll pass the key, first of all, and for the data, we're going to add a status of loading, then we'll add a payload of now. Cool.

    [10:59 - 11:15] Looks like I spot status incorrectly. So let's go ahead and change that status. We'll add semi colon here and an enter after the line 24. So what this means is just before the data starts fetching, the status will change to loading, which means it's actually loading.

    [11:16 - 11:48] And when the request is first made, or when the refresh button is first clicked, if there's nothing inside the cache, then it has a status of idle. I hope that makes sense. Okay, we solve one problem, but we have another one currently, our if statement is aborting the new controller, if the status is loading. But we want it to abort the previous request, not the current one. Because if you look at line 17, the new controller is set. But by line 21, if it has a status of loading, it will get aborted.

    [11:49 - 13:39] And we don't want to abort the current request that's made, but the previous one, so that the current one doesn't have anything to overwrite. So we'll have to store the controller inside our data cache, and only abort the previous request, if it has a status of loading, and a new request is made. So online 24, we'll go to the end of the line and inside our objects, we'll add a comma, and we're going to write controller. So this will be a new property that's added to our objects, and we'll set this to new controller. So what this means is that if a request is loading, it gets given a new controller, or an abort controller, so that if another request is made during the loading state, then we can cancel the controller that belongs to the previous requests. And since we only needs the controller for the loading state, when the data gets a status of successful error, we no longer need it. So we can scroll down to line 27, and inside the objects, we can add a controller and give it a value of undefined. Let's copy this controller and also put it online 30 when the status is error. Now let's scroll up. And we can move our if statement online 20 to 22 above the new controller that's created. This is so that we don't abort the controller we've just made. But online 18, by the time we get to that line, there's no variable called the controller. So what we have to do is get the existing controller from the cache. Let's go to line 16, get to the end of the line, hit enter, and we'll create a new variable called constant. And you can guess what I'm going to call this, we'll call it existing controller, and that will equal data cache dot get parentheses key, question mark dot controller.

    [13:40 - 13:52] This is because the controller might be undefined by this stage. Now we can update our if statement. So let's grab the existing controller text and we'll copy that and paste it inside the if statement.

    [13:53 - 14:59] So we'll say, if we have an existing controller, then we can add double ampers ands. And the data cache for that key has a status of loading. Then what we can do online 19 is remove our new controller and replace it with our existing controller so that it's aborting the previous controller. We can also add a console log or console error here. Let the user know what just happened. So we'll write console error, our parentheses and we'll say canceling the ongoing fetch for key, add a dollar sign and add k braces and we'll put the key inside here. Now I've noticed that ongoing is spelled incorrectly. So let's go ahead and change that. We're going to write going going perfect. Now if the request is actually aborted, what's going to happen is that the catch block here. So online 29 to line 32 will actually trigger since the fetch call is in progress and we've aborted it. Technically it won't resolve but instead it will reject.

    [15:00 - 15:30] And when it does reject, we don't want to set the cache data to a status of error since it's technically not an error. We want the payload and the status to stay the way it is since it's going to be updated by a new request. So we don't want to tell the user that there's been an error. Let's write some logic to handle this. At the end of line 29, we'll hit enter and we're going to say if our parentheses and in here we're going to write error, optional chain name because the name might not exist.

    [15:31 - 15:49] And if this is an error from the abort controller, it will have a name of abort error. Now if that's the case, we just want to return the code so that the code on line 33 and line 34 doesn't run. What we can do here as well is add another console error to tell the user what 's going on.

    [15:50 - 16:40] So we'll write console dot error and we'll add parentheses, add backtics and we 'll say fetch for dollar sign curly braces key was cancelled. Now if a request is aborted, we 'll log a message to the console and return. And if there's an actual error, then we'll log the arrow to the console and return that error inside the payload. Nice. We've actually added a lot of new code. So let's have a quick recap. Let's start from the top. We'll scroll up to our data, sorry, use data hook. And we can see that we've changed the status from loading to idle. This is because when the data is first fetch and there's no data in the cache, we want to give it status. But we don't want to set it to loading since the fetching process hasn't yet started. So we gave it a status of idle.

    [16:41 - 17:03] Then if we scroll down here, we can see that inside our status response, we have a status of idle, which has the same object as loading since when the user clicks on refetch or fetches the data, we want to show a loading spinner. But technically it's not actually loading. The loading actually takes place over here on line 25, where we set a status of loading.

    [17:04 - 18:06] But we also give it a controller. And this is because we want to have the ability to abort the call if another call is made with the same key. This will happen for calls that take a long time to respond. And another call is made by the user or by something like react. We then want to cancel the previous call and only let the new one continue. So we've given it a controller so that if the call is made and the status is loading, which we do check for up here, we can abort it with the signal and let the new one continue. So the new call will get a status of loading and also a new controller. I hope that makes sense. Let's move on and add our signal that we added here on line 23 to our Star Wars API file. At the end of line nine, which is all the way down here, we're going to add a comma, then add curly braces and we're going to put in the word signal. So the signal here is what is going to cancel the fetch call. But this means we also need to pass signal in as an argument on line one. So next ID will a comma and put the signal in here.

    [18:07 - 19:07] This also means that we need to pass the signal along with the ID inside our use data hook. So let's go to the data display controller. And here on line six, we'll go to the first parentheses inside the use data hook. And we'll type in signal. Remember, this argument is coming from the signal variable that we made. Now we can pass the signal as an argument inside our get data from ID function after the ID. Nice. And we should be all set. Let's go ahead and test this in the browser. Let's refresh the page by press command R. And we can see that the first call that's made is the call with an ID of one to the Star Wars API. If we press the fresh data button three times quickly, 123, we can see that the only call that is made is 76, even though an ID of 21 was requested and an ID of 26. Both of these were canceled. And only 76 got three. Very cool.

    [19:08 - 19:52] You may notice when you test this out, you'll get an error inside your console. And the reason I don't have that error is because I've removed errors here, right, so custom levels, I've hidden errors from my console. But if I turn them back on, we'll see that the errors are showing. This is an error that we wrote by hand. So I'm expecting this one. But this one actually comes from react. And I haven't yet figured out how to prevent this error. Or if it's even something that needs to be prevented, this seems to be the way react handles errors that come from the above controller. If we use regular JS, we wouldn't see this error, because there'd be a reason. And the reason would be something like the user has aborted. But for some reason, react thinks that there isn't a reason.

    [19:53 - 20:09] This error also appears if you abort the request using the use effect hook. So keep that in mind. Okay, now that we've addressed the issue with race conditions, in the next lesson, we're going to focus on addressing the issue with network waterfalls.