Fixing Network Waterfalls
Here we'll learn how to fix network waterfalls
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo 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.
Get unlimited access to React Data Fetching: Beyond the Basics, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:10] In the last lesson, we fixed the issue with a race conditions, and in this video, we're going to focus on network waterfalls. But let's first talk about what network waterfalls are.
[00:11 - 00:24] Imagine you're creating a Pokemon information app, something like a Pokédex, showing a Pokemon at the different locations that it can be found, along with other Pokemon in that location. To do that, you could make different calls inside nested components.
[00:25 - 00:49] So if we see here, there's the main component called Pokemon Details, and inside that component, there are multiple other components with the locations of each Pokemon, and inside the locations component there's also an Encounters component, and each component is making a different call to the Pokemon API. But in this specific scenario, not all the nested calls need information from the parent call in order to be made.
[00:50 - 01:05] However, because they're all nested, they create something called in-network waterfalls. So we can see here, the first component is rendered, and then the call is made, and then the next component is rendered, and then the call is made, and then the next nested component is rendered, and the call is also made.
[01:06 - 01:22] One way to fix this is instead of nesting the calls, you could hoist them or lift them up to the parent component, using the Contacts API to make the calls out at the same time. To replicate this issue, I've created a new project, which is based off the code from Lazar-Nicollov from Sentry.
[01:23 - 01:28] Let's go through it. All the code for this project is inside this app.jsx file.
[01:29 - 01:40] Of course, we have the utils folder with our data loader, and we have an API folder with the API calls that be made to the Pokemon API. So let's open this up and open the app folder up as well.
[01:41 - 01:49] We'll close the Explorer on the left, and we'll go through this app.jsx file. Let's wrap the text so we don't have to scroll horizontally, and let's go through the code.
[01:50 - 01:59] So in this file, there are multiple components. First we have the app component, and then we have the Pokemon Encounters component, and then we have the Location Details component.
[02:00 - 02:11] So we'll start with that component. What this is doing is making a call to the Pokemon API with an argument of Pikachu, and then getting the results from that call and setting it to the state.
[02:12 - 02:40] Then we have some code in the JSX to show loading text if we don't have any data, and once we have data, we can return the name from the data and then render the Pokemon Encounters component. Let's scroll down to line 22, open up the Pokemon Encounters component, and we can see down here on line 26 we're making another call to the Pokemon API, specifically to fetch encounters with an argument of Pikachu, then again we're setting that to the state using the set Encounters function.
[02:41 - 03:12] You can see here that both of the calls be made, so on line 26 and on line 8 needs an argument of Pikachu, but the results from this call are not needed to make this next call. Again, we're showing loading state on line 30 if we don't have any information, and when we do get information, we run some text so it can be found at, and then we map over each encounter displaying the encounter location name, and then in each encounter that's mapped over, we run the location details component.
[03:13 - 03:26] Now this specific component relies on the URL from the previous call, so the encounters call, and that is passed in as a prop along with the Pokemon name. Let's scroll down and open up the location details component.
[03:27 - 03:54] Here you can see this is making another call, this time to the location API, and then once we get this information to our Pokemon, in this case it's Pikachu, so we've got the Pokemon name, we filter out the information so that we only show information that belongs to our Pokemon, then we set that into the state. Let's scroll down and we can see in the JSX again we have a loading state on line 58, and then we have some text on line 61 called other encounters where we display the other Pokemon.
[03:55 - 04:05] Notice here on line 53, once we get the information from the map, we join them together and separate them with a comma. Cool, and that's it for this file, let's go into our Pokemon API file.
[04:06 - 04:34] Here there's nothing special, we have a fetch data function which is being used to fetch the information, so for a fetch Pokemon which is the first call that's made, we use the fetch data function to make this URL call along with a Pokemon name, and it's doing a simple fetch and we're telling the JSON. Please bear in mind we'll also log in each value, so fetch Pokemon is logging in the call on line 58, fetch encounters is logging the call on line 20, and similarly, fetch locations is logging the call on line 25.
[04:35 - 04:47] Ideally, both line 21 and line 16 should be using string literals, but I copied this code from the Lizzas article and this is the way it was. If you want to, you'll welcome to change it to match, but for now, we'll leave it as it is.
[04:48 - 05:01] Now that we have an idea of how the app works, let's take a look at this app in the browser. If we refresh the page, we can see in the console that first the Pokemon calls being made and then the Encounters call and then the Location call.
[05:02 - 05:07] Remember, the reason we have two console logs is because of strict mode. Let's take a look at the network.
[05:08 - 05:29] If we open this up and swab to the top, we can see that this call is made first before the encounters and then after the encounters has been made, then the Location call is being made. We can expand this by getting rid of unnecessary tabs, so we can get rid of size, we can get rid of time as well, and we can drag out the waterfall a bit more to see a bit more of the network waterfall in action.
[05:30 - 05:41] Let's also get rid of status here and we have a bit more room, we can drag this out as well and drag this out again. And we can see here that this call has to be made before that one and that one.
[05:42 - 05:48] Cool. Now that we have an idea of how the app works, let's see how we can improve it with our data loader.
[05:49 - 05:59] We can start by going to the main.jsx file and prefetching all of our data. Just like in the previous module, we can prefetch the data using our prefetch data function.
[06:00 - 06:05] Let's go ahead and do that. At the end of our fall, it ends a few times and let's write prefetch data.
[06:06 - 06:15] Then we'll add parentheses and for the first argument, which will be the key, we'll give it a key of Pokemon. Then for the second argument, we'll give it a arrow function, parentheses and then the arrow function.
[06:16 - 06:23] Then we'll write our fetch Pokemon function and here we'll add parentheses and give it an argument of Pikachu. Sweet.
[06:24 - 06:39] Now I've noticed things aren't being imported properly, so let's go ahead and address that issue. We'll first import the prefetch data function and we'll make sure that's coming from the utils/preloader.js file.
[06:40 - 06:52] Then we'll import the fetch Pokemon function and that is automatically coming from the API Pokemon.js file. We'll go to the end of line six and delete that closing bracket and we'll save the file.
[06:53 - 07:08] If we wanted to, we could copy the whole of line eight and run the call to fetch the Pokemon encounters as well. But the problem with that is that we'll have to run multiple prefetch data functions and that means the fetches won't happen at the same time.
[07:09 - 07:15] So we can address this by making the prefetch data function handle multiple fet ches. Let's remove line nine.
[07:16 - 07:25] We'll save this file and let's go into our data loader file to make a function that can handle multiple fetches at the same time. Let's scroll down to our prefetch on event function.
[07:26 - 07:34] That should be on line 41. Then at the end of line 43, we're going to export a new function which we'll call prefetch multi.
[07:35 - 07:40] Nice. This is going to take an argument we'll call data sources.
[07:41 - 07:51] And if we were in TypeScript, I would define the type of data sources as an array of objects with the key of name and function. But since this is just JS, we'll have to imagine that.
[07:52 - 08:01] Let's add curly braces at the end of this line and we'll press enter. Let's also press enter a few times at the end of line 48 to lift the function up so it's easy to read.
[08:02 - 08:20] If the data sources argument on line 46 is going to be an array of fetches, we can prefetch each one of those fetches using the array map method. So on line 47, we're going to create a new variable which we'll call requests will make that equal data sources dot map, then add parentheses.
[08:21 - 08:31] And here we're going to add another set of parentheses because we're going to have an arrow function. And this is going to run our prefetch data function, but we need to get the correct arguments.
[08:32 - 08:39] So we'll go back to the first set of parentheses. And here we're going to destructure the object just to get the key and the function value.
[08:40 - 08:54] Now, I know you have to imagine a lot at this moment, but when we're all done towards the end of this lesson, it will all make sense. Okay, so inside the prefetch data function, we're going to pass in the two arguments of key and function as well.
[08:55 - 09:07] Then we can use the promise dot all settled method to make all the requests at the same time. So we'll hit enter at the end of line 47 and writes promise with a capital P dot all settled.
[09:08 - 09:13] Then we'll add parentheses and we'll pass in our requests variable. Nice.
[09:14 - 09:27] The reason we're using promise dot all settled and not promise dot all is because we want all the responses to be made even if one fails. And surprisingly, this is all we have to do in our data dash load of file.
[09:28 - 09:35] Let's go back to the main dot JSX file and make use of our new prefetch multi function. Let's first create a variable called data sources.
[09:36 - 09:43] So at the top of line eight, we'll hit enter a few times. And then here we're going to write const data sources and we'll make that equal an array.
[09:44 - 10:01] And the first item in this array is going to be objects, which will have a property of key, which would be per command and we'll have another property of FM standing for function. And this will be an arrow function that will run our fetch per command function with an argument of Pikachu.
[10:02 - 10:11] Then we're going to add another item to this array. And this is going to have a key of encounters and a function, which will be another arrow function.
[10:12 - 10:22] And this will have our fetch encounters call with again an argument of Pikachu. Let's double check the relevant functions of being imported properly.
[10:23 - 10:30] And they are so fetch Pokemon and fetch encounters of both being imported online six. Then what we can do is we can use our prefetch multi function.
[10:31 - 10:39] So on line 19, I'm going to write to prefetch multi and then we can pass in our data sources variable. Sweet.
[10:40 - 10:44] Now we can get rid of the code on line 21. So if we don't need it anymore.
[10:45 - 10:51] And then that's all we have to do in this file. Now let's go to our app.dasix file and make if you changes.
[10:52 - 11:03] We're first going to change use effect and use our use data hook to get the data. Let's select and delete all the code online five, then writes const at curly braces, then write data, colon, per command.
[11:04 - 11:16] We'll also the structure of the is loading value as well, then we'll get our use data hook and make sure that's being imported. And here we're going to pass in a key of Pokemon.
[11:17 - 11:28] Now we can get rid of all the code from line 10 to line eight. And also online eight, we can get rid of this in the curly braces and replace it with our is loading variable.
[11:29 - 11:32] Cool. Let's do the same thing with the Pokemon encounters components.
[11:33 - 11:49] We'll scroll down all the way to line 20, then we'll select all the code online 20, get rid of it. All right, const curly braces data, comma is loading, then we'll write use data add parentheses and inside the parentheses, we're going to add encounters.
[11:50 - 12:02] Again, we'll scroll down and lead all the code from line 24 all the way to line 22. And also online 22, we're going to get rid of all the code in the curly braces and add is loading.
[12:03 - 12:10] And now where it says encounters on line 29, we can actually put the data in front of here. So we'll write data dot encounters.
[12:11 - 12:19] And there's no specific reason as to why we're doing that instead of changing the name of data. It's just because right now I don't feel like changing the name from data to encounters.
[12:20 - 12:32] But of course, you can go ahead and do that in your code. Now if you scroll down and focus on the JSX here for Pokemon encounters, again, you'll see that information is passed from that call into the URL prop.
[12:33 - 12:47] And that's the reason why we haven't preloaded the location data. So that is this API call over here, French location in the main.jsx file, because it depends on data from the encounters call.
[12:48 - 13:00] So what we'll have to do is edit the fetch encounters function in our Pokemon API file so that it can make all the relevant calls without having the need to make that call inside the component. Let's go ahead and do that.
[13:01 - 13:16] Now in the interest of time, I'm going to copy all the finished code, update this code in this file, and then explain what it's doing. So I'm going to select for the code from 21 to 19, delete it, and press command V to paste the code that I've copied from the completed project.
[13:17 - 13:21] Then I'm going to wrap the text so you don't have to scroll horizontally. And now we can take a look at the finished file.
[13:22 - 13:30] So first off, on line 20, you can see it's making the console log. And on line 21, it's doing a fetch call to the encounters part of the API.
[13:31 - 13:45] However, instead of just returning that data, we're doing a few more things. So here on line 22, we're looping over each of the results from encounters and just returning the location area dot URL.
[13:46 - 14:01] Then with that location URL data, or mapping over that data and using the URL to do a fetch location call, so that will give us a bunch of location information inside the locations variable. Then we're getting each location.
[14:02 - 14:11] So we're doing a flat map, which is a way to compress a nested map. So we're getting the locations and then we're filtering out the encounters that match our Pokemon name.
[14:12 - 14:31] So the name value is being passed in as an argument, imagine it's Pikachu, and then we're filtering out the names that don't match the name Pikachu. And then once we have the filtered out data from the encounters that have the name Pikachu, we're then looping over that or mapping over it and just returning the location name and the Pokemon name.
[14:32 - 14:42] And so once we have all that data inside our locations variable, then we can return an object of all the encounters and all the locations. I hope that makes sense.
[14:43 - 14:59] Feel free to pause the video, screenshot the code, rewind, fast forward, whatever you need to do to make sure that this file or this function makes sense. Now with this change in place, let's go to the app.jsx file and make some changes.
[15:00 - 15:18] Let's scroll up to the Pokemon encounters code. And here we need to make sure, so we'll scroll down to JSX, that we're only passing the location name to the location details component, since we already have the URL from the, from the Pokemon API file.
[15:19 - 15:30] So remember, we're getting all the encounters and getting the location URLs from the encounters. So you can just get rid of both of these props, replace it with name and that will equal curly braces.
[15:31 - 15:44] And here we're going to put an counter dot location underscore area dot name. So now we have the name of the location, we can scroll down into our location details component.
[15:45 - 15:53] And here, we actually don't need to use this user factor anymore. So we can get rid of all this code and including the state.
[15:54 - 16:04] And then online 41, we'll write const, we'll destructure data and then is loading. And to get that, we need to use the data hook with a key of encounters.
[16:05 - 16:10] So same as the key as in the encounters component. Now we can change this online 41.
[16:11 - 16:19] So the destructure of arguments from URL and Pokemon name, just a name. And here online 43, we can change everything in the statement to is loading.
[16:20 - 16:27] Now we no longer have a variable called other Pokemon. So let's change this code to return the correct information.
[16:28 - 16:32] We'll get rid of other Pokemon. And here we're going to write data dot locations.
[16:33 - 16:39] So we're getting the location information from the encounters call that we made . Remember, we have an object that has encounters and locations.
[16:40 - 16:52] And with this location information, what we're going to do is do a filter of the location based on the location name coming from the argument online 40. So we'll do location to an arrow function.
[16:53 - 17:01] And I'm going to do location dot location name, triple equals the name from our argument. Then once we have that information, we can map over that.
[17:02 - 17:11] And what we're going to do here is just return the Pokemon name dash API file. Line 28, we filter out the location name and the Pokemon name.
[17:12 - 17:18] But since we don't need to display the location name, we can just display the Pokemon name. So here, let's create an arrow function.
[17:19 - 17:27] And we can destructure the Pokemon name from the data. We'll copy that and we'll paste that here so that it just returns the Pokemon name.
[17:28 - 17:33] Now once we have the name, we want to separate each name with a comma. And we can do that with the join method.
[17:34 - 17:39] So add parentheses and the parentheses, we'll just add a comma and a white space. And that's it.
[17:40 - 17:49] Now, I'm sure there are ways you could make this code neater and you're welcome to do so after or during this lesson. But right now, we're going to go to the browser and see if our changes worked.
[17:50 - 18:05] Okay, so if we refresh the page and scroll up to have a look at the network tab , I'm going to make this a bit bigger so we can see what's going on with the waterfall. And you can see here that there's a tiny bit of a waterfall between the encounters and the location call.
[18:06 - 18:18] But we can also see that the call for the Pokemon and the call for the encounters happens at the same time. Now if there was a way to make all of these calls happen at the same time, then of course, I do that instead.
[18:19 - 18:23] But this is the best solution. And we do get rid of the excessive network waterfalls.
[18:24 - 18:38] I'm not sure if you noticed, but there are way less loading spinners when the page first loaded. If I disable the cache and do a hard refresh, so that's shift command R, we can see that there weren't as many loading spinners as there were with the code before we made any changes.
[18:39 - 18:55] Also, if we look at the blog post that I mentioned earlier from Lazar, we can see that the results from his solution, so using the context API is identical to the results that we produced using our data loader file. So I'm pretty happy with the code that we produced.
[18:56 - 19:01] And just like that, we've reached the end of another module. As usual, we've gone through a lot.
[19:02 - 19:03] the next lesson we're going to have a little recap.