In this talk, you will discover how to manage async operations and request cancellation implementing a maintainable and scalable API layer and enhancing it with de-coupled cancellation logic. You will also learn how to handle different API states in a clean and flexible manner.
Advanced Patterns for API Management in Large-Scale React Applications
AI Generated Video Summary
This Talk covers advanced patterns for API management in large-scale React applications. It introduces the concept of an API layer to manage API requests in a more organized and maintainable way. The benefits of using an API layer include improved maintainability, scalability, flexibility, and code reusability. The Talk also explores how to handle API states and statuses in React, and provides examples of canceling requests with Axios and React Query. Additionally, it explains how to use the API layer with React Query for simplified API management.
1. Introduction to API Management in React
Welcome to Advanced Patterns for API Management in Large-Scale React Applications. Today, we'll cover managing API requests in React, handling different API states, creating custom hooks, canceling requests with Axios and React query. Performing API requests in React can lead to code duplication and lack of reusability. To address this, we can implement an API layer. By creating a base API file and importing it into other API files, we can manage API requests in a more organized and maintainable way.
Hello, and welcome to Advanced Patterns for API Management in Large-Scale React Applications. My name is Tomasz Fienli and I'm a full-stack web and mobile developer with nine years of programming experience. I'm a co-owner of Fienli web tech and a mentor and consultant at CodeMentor.io. I'm also the author of Vue and React the Road to Enterprise books, as well as a technical writer for Telerik and the Road to Enterprise blogs.
Now, let's have a look at what we are going to cover today. So, first of all, we will start with how to manage API requests in React in a scalable and flexible manner with an API layer. Then we will see how to handle different API states while performing API requests. We'll also create custom hooks to manage API requests and states, as well as how to cancel requests with Axios and an API layer. Finally, we'll have a look at how to use React query and API layer and how to cancel requests with them.
Okay, so, how can we perform API requests in React? Actually, it's quite simple, right? We can, for instance, use Axios for that. We can just import it and use it in our components. And, well, it can work fine for, let's say, small applications. But there are some issues as the project grows, especially large ones, really. So, what are the main problems with this approach? First of all, code duplication and lack of reusability. Because imagine that we need to, for instance, fetch information about a user, let's say on logging page, register page, and the user profile page. So, all of these would have, well, the same code snippet. So, Axios gets a URL endpoint and so on and so on, right? So, that's not really reusable. And also, well, it's hard to maintain. Because if we had to make any changes to this code, like let's say, for instance, the URL endpoint change or we had to change the payload format, or, let's say, we had to migrate from using Axios to something like Firebase, right? We would need to visit every single component that is Axios and change them. Well, that's not really great, to be honest. So, let's have a look at how we can fix these issues by implementing an API layer. So, first, we would start with a base API file, where we would import Axios and create a new instance with some default configuration, like, for instance, a base URL. Then we have an API function that basically returns API wrapper methods around our Http client, in this case, Axios. And finally, we export it. Next, what we can do is, we can use this base API file and import it in another API file. So, for example, in this case, we'd have users API file. Because we want to do some things with users, like fetch information about users, add a new user, and so on. In this example, we have three methods, list users, get user, and add user. And as you can see, they all use the API methods from the base API file. And how would we use this in a component now? So, basically, what we would do is, we would import an API method from the API directory, and then just use it in a component.
2. API Layer Benefits and Flexibility
The API layer in React allows you to abstract away the details of API endpoints and easily switch between different HTTP clients. By separating the API logic into a separate layer, your components remain agnostic to the underlying implementation. This improves maintainability, scalability, and flexibility. You can easily add new API methods, replace the HTTP client, and enhance the API layer with custom logic. Code reusability is also increased, as API methods can be imported and used anywhere in the application. The API layer pattern is framework agnostic, making it suitable for various development environments.
So, as you can see, the component doesn't have to think about what kind of API endpoint has to be hit, right? The API method takes care of that. So, what about if we wanted to use a different HTTP client, like let's say, for instance, Firebase instead of Axios. So, our base API file would look a bit different. So, here, we would just import some necessary Firebase methods, would initialize the Firebase app. And then we would export what we need, for instance, Firebase, Firestore, which gives us methods to basically, well, connecting with the database, and so on, and other things like off storage functions, etc.
Now, in the user API file, we would again have the same methods, like list users, getUser, and addUser. However, in this case, we're using Firebase, of course, right under the hood. But let's have a look at how it would look like from the components perspective now. Well, actually, for the component, nothing would change, because it still would import the API method from the user's API file, and it would just execute it, right? So that's a really great thing about the API layer, because it's like a black box to your components. Because your components don't really have to care about what you use to perform requests. It only is concerned with what methods you should call, what kind of input it should provide, and what kind of output it can expect. As long as you can preserve this input and output contract, you don't need to make any changes to your components. You only need to make changes to the API layer, really.
Now, let's have a look at the benefits of the API layer. First of all, maintainability, as all the API-related code is in one place. Scalability, as you can easily add new API methods and files. And we also have flexibility, it's much easier to replace the HTTP client, and also enhance the API layer with custom logic. So as you've just seen, we replaced access with Firebase and we didn't need to make any changes to the component. Also code reusability, because API methods can be just imported and used anywhere in the application. And the API layer pattern is also framework agnostic.
3. Handling API States in React
Let's explore how to handle API states in React applications. Using Boolean flags for each state can lead to code duplication and complexity. We can improve this by using API states and implementing a useApiStateUseHook. The four API states are Idle, Pending, Success, and Error. By abstracting the states, we can manage API requests more efficiently.
Okay, so next let's have a look at how we can handle API states. So what I've seen in a lot of application is basically using Boolean flags. For instance, if you want to show a spinner, you would have isLoadingState. If you want to show an error, if for instance request failed, you would have an isError flag. And let's see if we want to lately initialize a request. For instance, if a user clicks a button, or if a user scrolls to a certain element, then we can also have isIinitialized flag. So as you can see here, we have three different states, and then all of them are updated accordingly.
The problem, however, and here as you can see, if it was initialized, it would display a button. If it's loading, we display a loading data message, canDisplaySpinner. If there was an error, then there was a problem. And finally, if everything went all right, we are displaying the data. But yeah. The problem, however, is that for each API state, we need a new state hook, right? Because we have initialized, it's loading, it's error. So that's already three states. That's only for one API request. If we need to make two requests, we might need to have six states. If we need to make three requests, we might need nine, and so on, and so on. But that's not really great.
So let's have a look at how we can improve it by using API states instead. And we will also implement a useApiStateUseHook to help us with that. So first of all, we have four different API states. Idle, which means that basically the request didn't start yet. And we have pending, which means that the request is being performed. And then we also have success and error. So obviously, success is for when request completed successfully, and error if there was a problem. We also have an array here, which basically just exports the constants, as we will need it in a moment. So let's go now to useApiStateUseHook. So first of all, we need to import useState and useMemo. And we also get the idle constant, as does our initial state for the hook. And we also get default statuses.
4. Managing API Statuses in React
We can use the useApiStateUseHook to manage API statuses in React. By using this hook, we can have a single state that holds all the API statuses, such as idle, pending, success, and error. We can easily update the API status using the setApiStatus method. By implementing a custom useAPI hook, we can further improve the API management in React. This hook accepts a method for executing API requests and a config object for setting initial data on the state.
And the reason why we need them is because we want to basically take all our statuses and then basically return an object with the statuses, like its idle, its pending, its success, its error, as you can see on the right side of the slide. And only one of them will be active at the time. So for instance, at the start, only isIdle will be set to true.
And now, here's our useApiStateUseHook. So we have our state for it. So in comparison to boolean flags, we have only one state that holds all the API statuses. And only one of them can be active at the same time. Then, we have the result of prepareStateUses. As I mentioned, it's an object that has its idle, its pending, and so on. And we use useMemo here so that it only re-evaluates if the API status changes. And then finally, we return an object with apiStatus, setApiStatus method, and all the normalized statuses.
Now, let's have a look at how we can use it. So as you can see here, we basically initialize the useApiStatus hook, and we can pass the idle there, though it's the default state anyway. If we wanted to start with pending immediately, you can do that. And then we destructure all the statuses, as well as the setApiStatus method, which then is used to basically update this API status accordingly. Before the request is started, we set it to pending. If it's completed successfully, we set it to success. If there's a problem, we set it to an error. And as you can see in the markup, we can basically just add ternaries, right? If it's idle, then we do something. If it's pending, then we show the spinner or loading message. If it's error, then an error, and so on and so on. So it's much cleaner this way. We don't need to have as many boolean flags. And the code is much cleaner and more concise.
So now let's have a look at how we can improve it even further by implementing a custom useAPI hook. So, first of all, we import useState again, as well as the useApiStatus hook that we've just covered, and three apiStatuses. So, pending, success, and error. Then useAPI accepts two parameters. The first one is the method that will execute an API request, and the second one is a config object. So, for instance, in this example, we can pass initial data inside of the config object and set it on the state for the data.
5. Managing API States and Requests
Inside the useAPI hook, the exec function handles setting API statuses, updating them based on the request status, and managing data and error states. The useAPI hook returns an object with data, API status, error, and exec method.
Besides the data state, we also have a state for the error, and of course we initialize the useApiStatus hook. Now, next inside of useAPI hook, we have the exec function. So, as you can see, what it does, it takes care of basically setting API statuses and updating them accordingly, depending on the status of the request. It also takes care of setting the data after the request finished successfully, and it will also handle setting the error if there was a problem, and it will clear it out if a request is supposed to be started again. And finally, the exec method returns an object with data and error if we needed to handle them. And last but not least, just return everything from the use API hook data, API status error exec and so on.
6. Using the API Hook and Canceling Requests
To use this hook, import it and pass the method that executes the API request. The hook returns an object with the API statuses, data, and an exec method. Depending on the status, appropriate markup is returned. To cancel requests with Axios and the API layer, enhance the API layer with abort logic. The with abort method accepts an Axios method and returns a function. The function creates a cancel method and token, enhances the config object, and executes the request. If there's an error, the isCancel method checks if the request was canceled. Wrap API wrapper methods with abort to complete the setup. For example, in a search box feature, an API request can be made for autocomplete.
And now, how can we use this hook? So, obviously, we need to import it and then just pass the method that is supposed to execute the API request. So, in this case, we're passing a method that will just return the result of getUser, which obviously comes from the API layer. And from the use API, we receive an object from which we destructure the normal statuses, the data and the exec method. And as you can see in the JSX, again, depending on the status, we just return appropriate markup. So, you know, for each idle, the button to initialize fetching for spending the spinner and so on and so on.
Okay, now, so how can we cancel requests when using Axios with the API layer? So, actually, we can enhance the API layer with abort logic. So, in the base API file, we would add the with abort method that would first accept the function. So, this function would be one of Axios methods, like Axios.get, Axios.put, post, and so on. Then, with abort returns a function, which, well, basically these arguments should be the ones that are passed to Axios method. So, for instance, URL, body, and config. And what we're doing there is we need to get access to the original config. And the reason for it is because as part of this config, we want to pass an abort property, which should have a function as a value. And what we're doing here, if abort is a function indeed, then we create a new cancel method and the cancel token using Axios' source method, and on the config object, we assign this cancel token, and, finally, we execute the abort method and pass the canceler there. And you will see what we did in a moment. And finally, after enhancing the config object, we just execute the request. We forward all the parameters besides the last one. The reason for it is because we don't want to pass the original config with the abort property, but rather we want to pass our own enhanced config object that doesn't have the abort property but might have a cancel token. And note that it's important that we use await here because if there was an error... Basically, without await, this error wouldn't be caught in the context of with abort. And so that's why we need await here. And then finally, if there's an error, we use access.isCancel method to check if the request was canceled. And if it was, we basically set an aborted property on the error object and we just throw the error feather so it can be handled outside. So let's... Okay, one more thing. We also need to wrap, of course, our API wrapper methods with abort. So like I mentioned before, we first pass access methods and then we initialize it again and forward all the parameters.
Okay, so here's how we can use it. Now, so imagine basically a feature like a search box. For instance, user can enter some query and there will be an API request made to fetch some information. For instance, for autocomplete.
7. Storing Cancellers and Handling Errors
To store the canceller method between re-renders, we use the ref. The optional chaining operator ensures that the canceller method is executed if it exists. The abort property in the config object receives the canceller method as an argument and assigns it to the ref. The catch statement checks if the error was aborted. The implementation details of the HTTP client are abstracted away, and we only need to provide the abort property.
So we have two states, one for the data that will be fetched from the API and one for the query that the user enters. And we also have the search about ref. So that's an important part here. Because the thing is that we need to be able to store the canceller method in between re-renders, right? So that's why we will put it on the ref. But first, let's just have a look at the onQueryChange function.
8. Using the API Layer with React Query
Using the API layer with React query is simple. Import the API method and the necessary React query hook, like useQuery. Pass the query key and the API method, then destructure the required data, refetch method, and statuses. Render content based on the status.
Okay, so now let's have a look at how we can use the API layer with React query. But actually, it's quite simple, because really what we only need to do is import the API method from the API layer. Also import whichever hook we need from React query, like for instance, use query in this case. And then we just use it. Right? We pass the key for the query, then we pass our list. Well, in this case, it lists users. So the API method. And we just disstracture what we need. So for instance, in this case, we get data. We get the refetch method, which can be used to initialize the request. And we also get our normal statuses. Again, and this part is basically the same as it was with, let's say, with our own use API hook, right? And depending on the status, we render appropriate content.