Understanding api calls in React functional components and related caveats

Gaurav Gupta
smallcase Engineering
5 min readJul 30, 2021

--

Easy to miss details in state management when consuming apis in React functional components, and some patterns to solve the issues encountered

Photo by Anelale Nájera on Unsplash

We have a complex react codebase, and we end up using a lot of different react patterns due to the underlying complexity of the product and the data. Recently we noticed a subtle bug when using an api call in the parent and passing the state of the api call to children. This post is based on some of the learnings from that bug, we will discuss things which are easy to miss when handling api state from inside React functional components.

Disclaimer: we are only going to discuss default react state management, no external libraries

Consider this component:

// Approach1function ComponentWithApi() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [data, setData] = useState();
useEffect(() => {
fetchData()
.then((res) => {
// try to change the order of the setData and setLoading here
// and see if the UI breaks
setData(res.data);
setLoading(false);
})
.catch((err) => {
setLoading(false);
setError(true);
setData();
});
}, []);
return loading ? (
"Loading..."
) : error ? (
"Error!"
) : (
<div> Approach1 value: {data.property} </div>
);
}

The component makes an api call in useEffect, and renders the loading / error / data state accordingly.

At first, this simple approach looks alright:

  • we start with loading as true
  • in render, we have covered all states
    - if loading — show Loading text
    - otherwise if error — show Error text
    - otherwise if not loading and not error, the data must be available, so show data

This works alright, but there are some subtle bugs in code and problems in understanding in the above setup:

  • I would personally like it better if the loading state is co-located with the api call, and not set as true by default
  • You may have an existing understanding that setState is async + batched in React, but if you change the order of the setLoading and setData, in the above code, you would see that the code breaks, and that the understanding does not stand true here.
  • once the code breaks, and you understand the reason (This requires a whole separate article, but TLDR; in the current implementation setState calls are only async inside event handlers but not inside async callbacks — promises / timeouts), you will also realize that there is actually a possible transient state where loading: false, data: undefined, error: false
  • I would personally like it better, if these connected states can somehow be handled together instead of having a useState for each of these separately

Let’s look at solving these problems one by one:

  • For the setState not being async inside promise, which leads to the transient loading: false, data: undefined, error: false state, we can do either of the 2 things:
    - make sure that the order of setState is correct inside the promise.then (we will see a better handling for this in the later pattern)
    - or handle the state for data not being unavailable explicitly. This also helps in setting the loading state initially to false, as that is the same as this transient state (which is one of our problems as discussed above)
// Approach 2function ComponentWithApi() {  // setting the initial loading state to false
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [data, setData] = useState();

useEffect(() => {
fetchData()
.then((res) => {
setData(res.data);
setLoading(false);
})
.catch((err) => {
setLoading(false);
setError(true);
setData();
});
}, []);
return loading ? (
"Loading..."
) : error ? (
"Error!"
) : data ? (
<div> Approach2 value: {data.property} </div>
) : null;
}

With the above approach, we have now solved these problems:

  • the loading state is co-located with the api call, and set initially to false
  • we have solved the unknown state, by explicitly handling it in render and not depending on react’s handling of async vs sync react state

We still have to solve the below problem / inconvenience:

  • instead of having a separate useState for each of these connected states, would make sense to update them all at once. This will solve our transient unknown state problem inherently, because that transient state would not happen when we update all related states at once (although a similar state would still happen in the initial render, so we would still need to explicitly handle data being undefined anyway)
// Approach 3const initialState = {
loading: false,
error: false,
data: null
};
const dataReducer = (state, action) => {
switch (action.type) {
case "LOADING":
return { ...state, loading: true, error: false, data: null };
case "LOADED":
return { ...state, loading: false, error: false, data: action.payload };
case "ERROR":
return { ...state, loading: false, error: true, data: null };
default:
return state;
}
};
function ComponentWithApi() {
const [api, dispatch] = useReducer(dataReducer, undefined, dataReducer);

useEffect(() => {
dispatch({ type: "LOADING " });
fetchData()
.then((res) => {
dispatch({ type: "LOADED", payload: res.data });
})
.catch((err) => {
dispatch({ type: "ERROR" });
});
}, []);
return api.loading ? (
"Loading..."
) : api.error ? (
"Some error occurred"
) : api.data ? (
<div>Approach3 Value: {api.data.property}</div>
) : null;
}

Now, agreed that the above approach looks a bit verbose, but these are some benefits you get out of the box with this:

  • allows you to update all the related state at once
  • allows you to potentially extract a custom hook for simple data fetching
  • feels more declarative, instead of orchestrating individual pieces of related state
  • If there are multiple api calls in the component, the separate api call states can be encapsulated in their own reducers, making the code much more readable.

Even with the above setup, there are some caveats to keep in mind if you are coming from a redux background

References:

--

--