How to make react router loaders wait until axios instance is initalized?

170 Views Asked by At

I have an axios instance that needs to be configured in order to start making normal API calls. I need to set authorization header and put 1 interceptor that will refresh the JWT token on every 401 request. Because I'm using hooks, I made it into a separate component that I'm afterwards nesting inside my App.tsx.

I'm using one of data routers from react-router-dom that supports "loader" functionality. The problem is that when I refresh the page at route (let's say "/profile") it firstly calls the loader function and only then all useEffect instances. It obviously results in 401 error, even though the user has been set in useAuth() hook by the time Axios Component was rendering.

That guy has been having similar issue like me, but I haven't found suitable answer in the comments
React Router Dom V6 loaders fire when router is created

As he states, I can even remove <RouterProvider/> from my App component return statement and it will still call loader before the useEffect. Even though no component of that router will be rendered afterwards.

Here's the video if it will make you understand the issue better:
https://youtu.be/MoV_924owTU

Any help will do!

AxiosSettings.tsx:

import axios from 'axios';
import useAuth from '../hooks/useAuth';
import {useEffect} from 'react';

const api = axios.create({
    baseURL: 'http://127.0.0.1:8000/api/',
    headers: {
        "Content-Type": "application/json",
    },
})

export function AxiosSettings({children}: { children: ReactNode }) {
    // Using this only because I need to call useAuth() hook in order to get user
    const {user, login} = useAuth()

    useEffect(() => {
        console.log('effect 1')
        if (user) {
            api.defaults.headers.common["Authorization"] = `Bearer ${user.accessToken}`
        } else {
            delete api.defaults.headers.common["Authorization"]
        }
    }, [user])

    useEffect(() => {
        console.log('effect 2')
        const interceptor = api.interceptors.response.use(...)
        return api.interceptors.response.eject(interceptor)
    }, []);

    return children;
}

export default api

My app component is structured like so:

const router = createBrowserRouter(createRoutesFromElements(
    <Route element={<RootLayout/>}>
        <Route index element={<Home/>}/>

        <Route path="register" element={<Signup/>}/>
        <Route path="login" element={<Login/>}/>
        <Route path="about"/>

        <Route element={<RequireAuth/>}>
            <Route path="profile" element={<ProfileLayout/>} loader={ProfileLoader}>
                <Route index element={<ProfileIndex/>} loader={PrizesLoader}/>
                <Route path="history"/>
                <Route path="invitations"/>
            </Route>
        </Route>

        <Route path="admin" element={<RequireAdmin/>}>
            <Route index element={<AdminPage/>}/>
            <Route path="game/:hash" element={<Game/>}/>
        </Route>
    </Route>
))

function App() {
    const [user, setUser] = useState<User | null>(() => {
        return SessionStorageUserService.get();
    })

    return (
        <AuthContext.Provider value={{user, setUser}}>
            <AxiosSettings>
                <RouterProvider router={router} />
            </AxiosSettings>
        </AuthContext.Provider>
    )
}
1

There are 1 best solutions below

0
Drew Reese On

The AxiosSettings could conditionally render its children only when the axios effects have run and set the headers and attached the response interceptor.

export function AxiosSettings({ children }: { children: ReactNode }) {
  const [isLoaded, setIsLoaded] = useState(false);

  const { user, login } = useAuth();

  useEffect(() => {
    console.log('effect 1');
    if (user) {
      api.defaults.headers.common["Authorization"] = `Bearer ${user.accessToken}`;
    } else {
      delete api.defaults.headers.common["Authorization"];
    }
  }, [user]);

  useEffect(() => {
    console.log('effect 2');
    const interceptor = api.interceptors.response.use(...);

    setIsLoaded(true);
    return api.interceptors.response.eject(interceptor);
  }, []);

  return isLoaded
    ? children
    : null; // <-- or loading indicator/spinner/etc
}

The RouterProvider will be conditionally rendered and it and the route the user is on won't be rendered until at least the second render cycle when the axios api instance has been configured.