Notes on a useAxios custom hook provided in a React tutorial video

Last updated on 15 Feb. 2024

Note: This post is a details-post for the post: Learning web app development through free online tutorials – Organized Notes and Log, https://raviswdev.blogspot.com/2024/03/learning-web-app-development-through.html .

The React tutorial video I am referring to is: React JS Full Course for Beginners | Complete All-in-One Tutorial | 9 Hours, 8 hr 49 min, published on Sep 10, 2021 by Dave Gray. It is part of my Roadmap to Learning Full Stack Web Development Beginner Level through free online tutorials.

This post is a longish one detailing some of the doubts I had and issues I faced with the useAxios custom hook and how I resolved them. I am giving below the final version of my code variation of the the useAxios hook (called useAxiosGet) for readers who want to view that first or view that only and skip the rest of the post.

App.js snippets and useAxiosGet.js source code on gist:  https://gist.github.com/ravisiyer/5760479a3bb4d4c70fe3c12ec2d66835 ... Also embedded below:
------

The tutorial code uses Axios CancelToken but the Axios documentation says that is deprecated.

Cancellation, https://axios-http.com/docs/cancellation : It says that signal can be used for terminating an axios call. In my variation, I plan to use signal. The above page also mentions a timeout property which seems to abort an axios request if it does not get done within the specified timeout period.

useAxios : A simple custom hook for calling APIs using axios, https://dev.to/hey_yogini/useaxios-a-simple-custom-hook-for-calling-apis-using-axios-2dkj

Following are some observations from usage of my variation of useAxios hook:

1) Every time app component is rendered (which will be initial creation time and every time its state variables change), useAxios hook is called. However the useAxios hook would only return its data state variables and not do an axios get every time it it called as that is controlled by a useEffect() in the useAxios hook which has only url as its dependency. The url does not change and so this useEffect() would be called only once* when the App component is created/mounted(if my understanding is correct) [*twice in dev. environment but only once in production env]. The net effect would be that axios.get is invoked only once* (twice in dev env.) when App component is created/mounted which is what we want.

In my variation, I observed that at times No Posts seemed to flash before list of posts was displayed at loading posts time. I put in some console log statements which confirmed that my PostsList component was called with 0 posts just after App component's useEffect called setPosts (triggered by data variable change). This was immediately followed by PostsList component being called with number of posts axios.get loads. Later sections of this post get into deeper analysis of why this is happening and how to prevent the "No Posts" flash at times during loading.

...

When in my variation of useAxios hook, I used controller.abort() in useEffect() cleanup function in useAxios hook, I get a message, "Get Data error. Details: canceled" to appear briefly when app is loaded/reloaded (url is refreshed in browser), in Strict Mode (only). My understanding is that useAxios hook's useEffect is invoked twice on component creation (mounting) in Strict Mode. So first setup and then cleanup and then setup again (but not cleanup I presume as this second cleanup would happen only when component is removed/unmounted.) The first invocation of cleanup would execute controller.abort() which may be canceling the axios.get call (if it has not returned by then), which in turn seems to invoke the catch part of the trycatch block having the axios.get call. In this catch part setAxiosGetError(error.message) is executed which may be setting it to "canceled".

The tutorial code seems to fix a similar issue it may be facing with its axios cancel code in its cleanup function using an isMounted variable which is set to true at beginning of useEffect setup function and set to false at beginning of cleanup function. Further the data and fetchError state variables are changed only if isMounted is true, and so even if the catch part of its try block having the axios.get call, is executed due to cleanup executing axios cancel code, the catch part does NOT change fetchError state variable as isMounted has been set to false by cleanup. 

Note that useEffect setup and cleanup functions are called not only on page mounting and dismounting but whenever the dependencies change (see Notes on React useEffect, https://raviswdev.blogspot.com/2024/02/notes-on-react-useeffect.html ). In the tutorial code, the dataURL does not change and this is the only dependency for the useAxios hook's useEffect code. So re-render of the App component will not trigger a call of useAxios hook's useEffect setup or cleanup function.

Hmm.
Some more info. on this issue:
See Bug: React.StrictMode causes AbortController to cancel #25284, https://github.com/facebook/react/issues/25284
The section "How to use it in React with a useEffect hook?" in https://www.j-labs.pl/en/tech-blog/how-to-use-the-useeffect-hook-with-the-abortcontroller/ has example code which I think will face the same issue I face (it will console log an error in strict mode).

Wrote some test code to check order in which statements immediately after abort in cleanup and catch block execute. First the statement after abort in cleanup is executed, then the catch block code is executed. [Later update: It seems that the abort in cleanup results in catch block in next setup cycle code! See relevant console log snippet provided later on in this post.]

In my variation, I used a cleanupAbort boolean variable inside the useEffect() hook which is initialized to false. In cleanup() function, just before controller.abort is called, I set cleanupAbort to true. In the catch block of the try-catch statement having the axios.get call, I first check if cleanupAbort is true and if so I know that this error is due to cleanup() having called abort and so I ignore it and set cleanupAbort to false. In the else part of the statement where I know that this is not due to cleanup() having called abort, I set the axiosGetError state variable to the error message given by axios (error.message). This fixes the problem of "Get Data error. Details: canceled" appearing briefly when app is loaded/reloaded. Instead, as wanted, the Loading... message appears which goes away on the data getting successfully loaded or, if there is an axios.get error, the axios error message appears (not canceled message but, for example, "Network Error" if the JSON server is not running) (and the Loading ... message goes away).


Two (possible) optimizations:
1) cleanup() can first check if isLoading which will be true if axios.get call has been made, and only then call abort ... Issue is that checking isLoading() gives a warning: "Line 43:6:  React Hook useEffect has a missing dependency: 'isLoading'. Either include it or remove the dependency array". So I used an axiosGetInProgress local variable in the useEffect, setting it to true just before calling axios.get and setting it to false immediately after. In cleanup I checked axiosGetInProgress instead of IsLoading.
2) The order of calling setup() and cleanup() functions is setup followed by cleanup followed by setup ... As setup initializes cleanupAbort to false, the catch block need not really do that. ... However, I still do not understand the React useEffect() functioning clearly and so I think it is better I play a little safe and have catch block also setting cleanupAbort to false (if it is true).

==========

Digging deeper into brief display of No Posts issue

I commented out Strict Mode in index.js for these tests & observations.
See below pic (useAxiosIsLoadingIssue1.jpg).

[On PC desktop/laptop, to open pic in larger resolution (if available), right-click on pic followed by open link (NOT image) in new tab/window. In new tab/window you may have to click on pic to zoom in.]



Copy-paste of its console log:
Navigated to http://localhost:4000/
App.js:16 In App: About to call useAxiosGet
Home.js:6 In Home: isLoading = true
useAxiosGet.js:10 useAxiosGet: useEffect: setup fn.invoked
App.js:21 App: useEffect: Just before calling setPosts()data.length = 0
App.js:16 In App: About to call useAxiosGet
Home.js:6 In Home: isLoading = true
useAxiosGet.js:22 useAxiosGet: useEffect: just before setData(). res.data.length=5
useAxiosGet.js:38 Setting isLoading to false
App.js:16 In App: About to call useAxiosGet
Home.js:6 In Home: isLoading = false
PostsList.js:4 In PostsList: posts.length = 0
App.js:21 App: useEffect: Just before calling setPosts()data.length = 5
App.js:16 In App: About to call useAxiosGet
Home.js:6 In Home: isLoading = false
PostsList.js:4 In PostsList: posts.length = 5
....

Observations:

The issue seems to have the following sequence:
1) useAxiosGet returning from axios.get (await), setting data state variable with returned posts and setting isLoading to false
2) App component re-rendering. This perhaps is triggered by change in data state variable as useAxiosGet function was called by App component and so I think App component is tied to the state variables in useAxiosGet. See section "State(s) in custom hook are linked to component which uses hook" later on in this post.
3) In this App component re-render first the App component code is run. So useAxiosGet is called which returns isLoading as false and updated data (and axiosGetError). This is followed by the rendering of App component (return statement code) where isLoading and posts are passed as props to Home component. Now isLoading is false but posts still has the old data and so empty array (no posts) as App component's useEffect has still not run after data has been updated. Home component logic then shows "No posts".
4) Then the App component's useEffect which has a dependency on data (which has changed) is run. This updates the posts state variable to value of data. So now the posts state variable has the posts data returned by axios get.
5) Then App component is re-rendered (immediately after). This seems to be due to posts state variable of App component having changed in previous step by App component's useEffect.
6) In this App component render, useAxiosGet is called again. It returns same values of false for isLoading and for data. Home component is rendered with props of isLoading (false) and posts (updated with data from axios get). So now Home component shows the list of posts and the "No posts" message goes away. As this re-render of App and Home components happens very quickly after previous render (having "No posts" message), the "No posts" message is shown very briefly (flashes).

The question is the order in which App component executes its code and executes useEffect for data change. From https://legacy.reactjs.org/docs/hooks-effect.html (in the context of a useEffect that does not specify a dependency array) "Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects." Thus it is clear that useEffect() runs after the component is rendered. This tallies with above observations.

One solution to "No posts" flash during posts loading

A solution that I have used in my code variation is to have a different state variable arePostsLoading in App component. The core of the issue is that isLoading of useAxiosGet accurately reflects whether the data object (array) returned by useAxiosGet has the axios.get returned data loaded or not. But what the Home component needs as a prop is whether the posts props passed to it has posts data (different from useAxiosGet returned data object) loaded or not. So arePostsLoading is the new state variable that accurately reflects whether posts state variable has its data loaded or not, which is passed as a prop to the Home component.

arePostsLoading is initialized to true when defined. It is later set to false by App component's useEffect after it has executed setPosts(data). Home component is passed arePostsLoading state variable and posts state variable.

Also in the useEffect I decided to confirm that isLoading (aliased to isDataLoading while destructuring) is false before calling setPosts(data). I think this may be a safer way. What if axios.get returns empty data (array)? Then the useAxiosGet returned data does not change but we need to set arePostsLoading to false. So I added the check and also added isDataLoading as a dependency to App component's useEffect. I also confirmed that the code works as expected (removes Loading message and shows No Posts message) if the JSON file has no posts.

In the tutorial code and in my variation code, the url parameter of useAxiosFetch/useAxiosGet does not change. I think that if one invokes useAxiosGet with a changed url, prior to that arePostsLoading should be set to true, as useAxiosGet's useEffect will run due to changed url and an axios.get request will be made.

Associated source code: First file is related snippets from App.js which is followed by useAxiosGet.js code in full:


---


When I tried out (current version of) above code in Strict Mode (it was commented out earlier), the No Posts flashing issue cropped up again! The console log statements below show the issue.
--- start console log snippet ---
Navigated to http://localhost:4000/
Home.js:6 In Home: arePostsLoading = true
installHook.js:1 In Home: arePostsLoading = true
useAxiosGet.js:10 useAxiosGet: useEffect: setup fn.invoked
App.js:23 App: useEffect invoked
useAxiosGet.js:41 useAxiosGet: useEffect: cleanup fn.invoked. axiosGetInProgress = true
useAxiosGet.js:47 useAxiosGet: useEffect: cleanup: Just before calling controller.abort.
useAxiosGet.js:10 useAxiosGet: useEffect: setup fn.invoked
App.js:23 App: useEffect invoked
useAxiosGet.js:25 useAxiosGet: useEffect: setup: catch block beginning
useAxiosGet.js:34 useAxiosGet: useEffect: setup: finally block: calling setIsLoading(false)
Home.js:6 In Home: arePostsLoading = true
installHook.js:1 In Home: arePostsLoading = true
useAxiosGet.js:34 useAxiosGet: useEffect: setup: finally block: calling setIsLoading(false)
App.js:23 App: useEffect invoked
App.js:25 App: useEffect: Just before calling setPosts() data.length = 0
Home.js:6 In Home: arePostsLoading = false
installHook.js:1 In Home: arePostsLoading = false
PostsList.js:4 In PostsList: posts.length = 0
installHook.js:1 In PostsList: posts.length = 0
App.js:23 App: useEffect invoked
App.js:25 App: useEffect: Just before calling setPosts() data.length = 5
Home.js:6 In Home: arePostsLoading = false
installHook.js:1 In Home: arePostsLoading = false
PostsList.js:4 In PostsList: posts.length = 5
installHook.js:1 In PostsList: posts.length = 5
--- end console log snippet ---

Another point to note here, is that the abort call made in useAxiosGet hook's useEffect cleanup, seems to be caught in the next useEffect setup! I had presumed that it probably was getting caught in the setup code in that cycle itself but without a clear idea of why that was so. Now that I know that it is caught in the next useEffect setup, I need to analyze the approach carefully and see whether that will trip up the code.


State(s) in custom hook are linked to component which uses hook

Following article gives a very detailed account of component re-renders in the context of React hooks (but not custom hooks, it seems from my quick look). I have not gone through it in detail as it will suck up a lot of time. But I may go through it later on: React Hooks - Understanding Component Re-renders, https://medium.com/@guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928

"Using a custom hook allows for the reuse of stateful logic across various components. However, it's important to note that all states and effects within the custom hook are linked to the component where it's utilized. Consequently, any changes in state within the custom hook will cause the component using the custom hook to re-render, regardless of whether the custom hook returns anything or not."

I confirmed the above with a test program ... Source code and program console log output given below (filename: AppCustomHookStateRerender.js):
---

....
Reusing Logic with Custom Hooks, https://react.dev/learn/reusing-logic-with-custom-hooks has many code samples and associated description. Some that I found to be of interest in the context of this useAxios notes post are:
1) In section "Custom Hooks let you share stateful logic, not state itself", https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-let-you-share-stateful-logic-not-state-itself : useFormInput custom hook that gets used two times by a Form component and each such usage has its own state variable (called value in useFormInput). The page states, "Custom Hooks let you share stateful logic but not state itself. Each call to a Hook is completely independent from every other call to the same Hook."
2) In section "Passing reactive values between Hooks", https://react.dev/learn/reusing-logic-with-custom-hooks#passing-reactive-values-between-hooks : "Because custom Hooks re-render together with your component, they always receive the latest props and state." It has a chat room example with a useChatRoom custom hook to illustrate this.

...

In Strict Mode, react not only re-renders components an extra time but also runs effects an extra time. See https://react.dev/reference/react/StrictMode .

In Strict Mode, at the beginning the App component seems to be rendered twice and then the useAxiosGet hook setup is run, followed by its cleanup and then its setup is run again. In contrast, when Strict Mode is off, the useAxiosGet hook is setup is run once only. So it is only in Strict Mode that the axios request abort code runs.


Fetching Data in React with useEffect, https://maxrozen.com/fetching-data-react-with-useeffect 

Fixing Race Conditions in React with useEffect, https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect ... Has an abort example!!! So very useful for me. Related MDN ref page: https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort which states, "When abort() is called, the fetch() promise rejects with an Error of type DOMException, with name AbortError." 

When I checked, my axios code returns error.name as "CanceledError".
....


...

After I added a random number to each useAxiosGet setup function which is also accessed in the cleanup function and which is printed out in some console log statements, I noted that in React Strict Mode, as expected, the useEffect of useAxiosGet has its setup function called followed by its cleanup function, and that is followed by its setup function again. But this created, at least in some cases, a situation where the cleanup function executes controller.abort but the catch handler of the setup is invoked (for this abort), only after setup function has run again (this time with a different random number generated). To put it in other words, for a time, two instances of useAxiosGet useEffect setup are running concurrently. Note that setup function invokes an async function which does await on axios.get, and which seems to be the root cause of these two instances running concurrently. 

But these two instances update the state variables of useAxiosGet which, if my understanding is right, is common among them. This trips up the logic in the useEffect setup and cleanup functions related to update of state variables.

Clean Up Async Requests in `useEffect` Hooks, https://dev.to/pallymore/clean-up-async-requests-in-useeffect-hooks-90h 
Above article states that the catch block (in the setup function of the useEffect in the hook) should check for (!abortController.signal.aborted) and only if that is true (not aborted), should some key code run. 

I tried that approach and it seems to have solved the issue. It works for JSON server not running case (Network Error), JSON server with non-empty posts data and JSON server with empty posts data.
In this approach, essentially, the catch block of first invocation of setup function does not change state variables set by second invocation of setup function, if the first invocation axios.get has been aborted (by our own call to controller.abort).

I also coded reset of state variables to initial state (before setup function changed them) in the cleanup function of both useEffect in useAxiosGet hook as well as in App component. The useEffects and related code seem to look quite logical and clean now.

Source code of this final version of my variation of useAxios hook on gist:  https://gist.github.com/ravisiyer/5760479a3bb4d4c70fe3c12ec2d66835. Embedded source code for this version is provided at the top of this post.

Another solution to afore mentioned issue may be to block the 2nd setup function invocation before its main code runs, waiting for the 1st setup function invocation to finish. But that seems to be a complex solution to the issue given that the above mentioned solution (catch checks for abort and does not change state variables if abort is true) works.
...
Tutorial useAxiosFetch has initial value of isLoading state variable set to false! Will that not lead to "No Posts" flash during first rendering of App component (before useEffect of useAxiosFetch is run)? On going through the source code of its App and Home components, I think it will. On playing the video at 0.25 speed, I could get a screenshot of the final version showing the "No posts to display." message at 7:16:38 into the video. It flashes and disappears very quickly. So that confirms that the tutorial code does flash the "No posts" message.

I was able to run the tutorial code (using older react and react-router-dom packages that it uses) and confirm by setting a breakpoint in its Home component return statement that it does show "No posts to display" before it is overwritten by Loading message.

In the tutorial code, checking the isMounted variable (which is set to false in cleanup) seems to ensure that state variables would not be changed by catch block of its useEffect setup function if the catch is invoked after cleanup (which would typically be the case as catch calls axios cancel).

Comments