7 questions to find out corner cases when fetching data from an API in a React application.


Anyone can write a basic fetch request and get some data back. But what other things should be considered? What are more production-grade requirements?

Fetching data from APIs is something that almost all React applications do. However, this doesn't mean doing it correctly is easy. While it's straightforward to fetch data and show it on the screen, it's not trivial to get all the corner cases right.

In this article, we'll go over 7 questions we can make to find out corner cases when fetching data from an API. We'll start with an overview of all of them, and then go into detail on each one.

Questions

  1. Are we canceling API requests when the response is no longer needed?
  2. Are we handling all possible error and success cases?
  3. Can the application handle exceptions thrown from API requests?
  4. Is it possible for the user to get stuck on a loading screen?
  5. Is the application ready to deal with API requests that may take a while to complete?
  6. When will it be necessary to fetch the data again?
  7. Are we handling race conditions?

Are we canceling API requests when the response is no longer needed?

When the result of an API request isn't needed anymore, we should cancel it. Otherwise, we might run into errors like Can't call setState on an unmounted component, or we might end up showing the user information that isn't correct.

There are multiple ways for the data returned from an API request to become unnecessary. I find out that the most common ones are when the component that was going to store the data is no longer mounted, and when the state/props used to make the request or handle its response are no longer up to date.

Are we handling all possible error and success cases?

When fetching data from an API it's easy to think of successes and errors as rendering data on the screen or showing a generic error message. However, it can be a lot more complicated than that.

We might not want to handle all success responses equally. As an example, we might be fetching data from a collection. What happens when the collection is empty? We may want to redirect the user to another page or render a custom screen just for that situation. Another example is implementing infinite scroll. The user may, eventually, reach the end of the feed. How will the application handle that?

The same is true for errors. For some errors, a generic try again message might be the only solution. However, other errors may require different approaches. If we're making a request to an API that needs authentication, we might have an error due to an expired session. On an expired session, it might be a good idea to tell the user that his session has expired and that he'll have to log in again. Other errors might be sporadic, and it might be a good idea to retry the request before showing an error message.

Handling the variety of responses from an API can have a significant impact on the design of the application. So it's essential to be aware of them when implementing a solution.

Can the application handle exceptions thrown from API requests?

It's easy to forget that the API we're using to make requests may throw an exception. To avoid the application crashing horribly in the hands of our users, it's a good idea to .catch() those errors or to surround them in a try-catch block.

Is it possible for the user to get stuck on a loading screen?

When it's necessary, we rarely forget to set a component state to loading when we're going to fetch data. But what about removing that loading state? When we get our code ready to handle exceptions and error responses, depending on how we designed our code, it's easy to let an execution path slip by that doesn't remove the loading state. Better to keep an eye out for it.

Is the application ready to deal with API requests that may take a while to complete?

Due to a server receiving a lot of traffic at the time of the request or simply because the API request triggers an operation that takes a while to complete, the user might be left watching a loading screen.

To prevent a depressing UX, we need to be aware of when loading might take a while and choose an appropriate strategy to mitigate it. Common strategies involve setting a timeout for the request to showing an entertaining UI to make time go by faster.

When will it be necessary to fetch the data again?

As soon as we get data back from the server, it might already be stale. If that's a problem or not depends on the domain and nature of the application and data.

In real-time applications, there's a high effort to keep data in sync between server and clients. But on a blog, we can wait for the user to navigate back to the page to show him the most up to date articles.

The crucial thing here is to analyze the domain and nature of the application and data and look for situations where the user not seeing the most up to date data might be problematic.

Are we handling race conditions?

A common pitfall when making API requests that may run in parallel is to forget that the order in which we made the requests, is not necessarily the same order in which we'll get the responses. For example, if we make request A followed by B, we might get the response from B first than A. Depending on the context, this might lead to inconsistent and buggy behavior.

A classic example where this might be an issue, is when making a search box with auto-complete. If we make an API request each time the search box query changes, we'll end making multiple requests that will run in parallel. When we're not careful to display the response related to the most up to date query, we may have a response from an older query override the most recent one.

Conclusion

Correctly handling all the corner cases of fetching data from an API can be hard. One way to deal with it is to have strategies we can systematically apply and rely on. These seven questions are part of my strategy to deal with fetching data. Another useful strategy is to send logs made on the application to a server for later analysis. Analyzing logs can help track undesired behaviors.

Next time you have to fetch data for an application, give these questions a try and see how many corner cases you uncover because of them.

Programming with strategies and principles is useful regardless of what we're building. I'd say it is the cornerstone of consistently making high-quality software. If this idea resonates with you, subscribe to the newsletter at the end of this page. I'm always trying to improve my strategies and development processes, and I share my findings in this blog.

I hope this article is useful to you, and feel free to hit me up on twitter if you want to talk about it :)