I just spent 10 HOURS debugging a simple issue while attempting a quick refactor to my authentication layer in React Native, only to learn a hard truth about React setState and Axios behavior... you won't believe it.
In my codebase, I want to have an authenticated API client, one that uses Axios under the hood with default headers and authorization (bearer token). We can create this Axios instance once the user logs in. Doesn't sound too hard right?!
So instead of having all my API calls manually pass in the bearer token, I used React's createContext/useContext to store an instance of an Axios client (AxiosInstance).
I create the Axios client by using axios.create({ defaults }) and then update the state within my context provider component with the new API client with authentication by default: setClient(apiClient);
All sounds easy peasy so far right? This is what I thought too, until I spent all day and half of another day figuring out what the heck was wrong with my approach and why the behavior was NOT working like I expected. Now what wasn't working you might ask? Seemingly everything...
The behavior I was seeing: after you log you land on the "Friends" tab which should make a request to my Laravel API /api/v1/friends but it was instead making a request to "/" so I thought something was off with my Axios client options, since it worked before I attempted to make this axios client via axios.create and just used the global axios.get.
In my custom client I only have default headers, bearer token, and an interceptor for request / response. Nothing crazy complicated but I spent hours there tweaking it and to no success: still getting "/" as a request.
I debugged in Laravel Telescope and constantly saw my API not listening to my endpoint I provided in my JS code. I went crazy, it's just a STRING value, why is it not behaving? Debugger attached, console logs, all verified the string was passed correctly.
I'm new to React Native / Expo, so I spent HOURS nuking my dev environment repeatedly, pod installing, expo prebuild, rm -rf node_modules..., you name it, all thinking it wasn't picking up my endpoint string value, but it just can't be.
Another weird thing I was seeing was that it was making a request to "/" but when I called apiClient.get('api/v1/friends') it was saying method not found.
But when I created my Axios API client, the method was found at tie of creation, and I wasn't changing it at ALL anywhere in between. I threw in useEffects here and there, async's here and there, nada.
I clearly was returning the right Axios instance but no dice according to my final build, so what's next?
I then thought I was using React's Context wrong, so dove into the docs there to actually learn instead of just use and assume it's all gravy. Turns out everything I did was right (fwiw I copied things from expo docs to begin with).
Spent a few hours there too: I learned a few things but didn't get me any closer to my solution.
Now I went onto a different approach: how about I store my Axios API client in Zustand? My other state management library.
Super simple change: add a new property to my store, to store my Axios instance there as well, and use that instance in my Friends page to make the request, instead of the Context provider. To my surprise: it worked!? Ok so what's going on then?
When I create my API client and set it within my context, I noticed the console.log prints out: "[Function wrap]" but then later when I use my API client in my Friends tab page, it prints out "{"_h": 0, "_i": 0, "_j": null, "_k": null}"... huh?!
So I Google what the heck "{"_h": 0, "_i": 0, "_j": null, "_k": null}" is and it turns out it's a promise. Ok, but my Axios instance isn't a promise, it's a simple synchronous return value from the axios.create function.
It looks like the value of my state is changing, without me even doing anything, React bug? I wasted a few hours here tinkering with no changes in behavior.
So I dive into React useState/setState: reading docs from top to bottom. I notice that the docs call out something important when calling setState():
"If you pass a function as `nextState`, it will be treated as an updater function. It must be pure, should take the pending state as its only argument, and should return the next state"
And it hit me: when reading Axios docs, there is the "Axios API" page which shows you can do axios.get(), axios.put(), etc. OR you can do axios({ params }). Not sure why you'd want to do the raw way, but you can.
This means the return type of the axios.create function is indeed a function that takes in params, which will hit React's "update function" treatment (confirmed with logs I saw earlier: [Function wrap]).
So I:
1. Create my Axios "instance" via axios.create: const apiClient = ...
2. Store it in my context's state: setState(apiClient)
3. Use it in my Friend tab page component later: apiClient.get('/api/v1/friends')
But my state changes without me doing anything between #2 and #3, since React is actually CALLING my axios instance as a function, since it has a function like return type. Essentially calling: apiClient() which is: axios({}), invoking a GET request with no parameters.
So THAT is why I was seeing a request for "/"! Thanks updater function!
Not only that, but since the updater calls AND sets the state from the return value of the updater function, my Axios instance state was replaced with a promise, since that's the return type of any Axios request.
This explains why I was seeing:
[Function wrap]
to:
{"_h": 0, "_i": 0, "_j": null, "_k": null}
(^^ this is the console.log value of a promise, only found from me Googling what is that cryptic string)
So now I realized my whole state is getting ruined since Axios returns a function for ease of use?, which gets invoked by React's setState internals and then overwritten, since it's seen as a setState updater function. What's the fix?
Simple answer of course: wrap Axios instance in an object, so it's not seen as an "updater" function anymore.
Behavior as I now realize it:
1. Create apiClient via axios.create
2. Store in my context's state: setState(apiClient)
3. React called the apiClient as an updater: setState(const retVal = apiClient());
4. Calling the apiClient() was sending a bare "/" request
5. React was updating my apiClient state to be the promise value of that get request
I banged my head against a wall figuratively for 10 HOURS, only to figure out axios.create and setState have a bad synergy, if you are not careful. I hope this helps anyone else, who spends hours and hours trying to figure this out 😅 the joy of working with software.
This one was truly humbling as I questioned if I knew anything at all with weird debugging behavior as well as me stepping into React Native / Expo for the first time. A small refactor: "should take 30 mins" ended up taking 10 hours, jeeeeze.
I definitely learned quite a bit in the process, but it's painful none-the-less. Onto the next hopefully 🤞small refactor