How to use Axios cancelToken in interceptors? - reactjs

In ReactJs I am using Axios to getting data from API. I need to use cancelToken when I try to make the duplicate requests. E.g: suppose I am on the homepage before complete Axios request, I am requested for About page. As a result, the React app showing memory leaking error. So, my plan is to set Axios cancelToken in Axios interceptors. I have tried but, it is not working for me.
requestApi.js
import axios from 'axios';
const requestApi = axios.create({
baseURL: process.env.REACT_APP_API_URL
});
const source = axios.CancelToken.source();
requestApi.interceptors.request.use(async config => {
const existUser = JSON.parse(localStorage.getItem('user'));
const token = existUser && existUser.token ? existUser.token : null;
if (token) {
config.headers['Authorization'] = token;
config.headers['cache-control'] = 'no-cache';
}
config.cancelToken = source.token;
return config;
}, error => {
return Promise.reject(error);
});
requestApi.interceptors.request.use(async response => {
throw new axios.Cancel('Operation canceled by the user.');
return response;
}, error => {
return Promise.reject(error);
});
export default requestApi;
Dashboard.js
import requestApi from './requestApi';
useEffect(() => {
const fetchData = async () => {
try {
const res = await requestApi.get('/dashboard');
console.log(res.data);
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);

in case you still need it or if someone else comes looking for this. This is how it has worked for me.
import axios from "axios";
// Store requests
let sourceRequest = {};
const requestApi = axios.create({
baseURL: process.env.REACT_APP_API_URL
});
requestApi.interceptors.request.use(
async config => {
const existUser = JSON.parse(localStorage.getItem("user"));
const token = existUser && existUser.token ? existUser.token : null;
if (token) {
config.headers["Authorization"] = token;
config.headers["cache-control"] = "no-cache";
}
return config;
},
error => {
return Promise.reject(error);
}
);
requestApi.interceptors.request.use(
request => {
// If the application exists cancel
if (sourceRequest[request.url]) {
sourceRequest[request.url].cancel("Automatic cancellation");
}
// Store or update application token
const axiosSource = axios.CancelToken.source();
sourceRequest[request.url] = { cancel: axiosSource.cancel };
request.cancelToken = axiosSource.token;
return request;
},
error => {
return Promise.reject(error);
}
);
export default requestApi;

This may not be a stable solution, but we can use some magic to make a component that terminates async code (including requests) running inside when the component unmounts. No tokens required to make it work. See a Live Demo
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
function* makeAPICall(url) {
const existUser = JSON.parse(localStorage.getItem("user"));
const token = existUser && existUser.token ? existUser.token : null;
return yield cpAxios(url, {
headers: {
Authorization: token,
"cache-control": "no-cache"
}
});
}
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
console.log("mount");
this.timeout(props.timeout);
try {
setText("fetching...");
const response = yield* makeAPICall(props.url);
setText(`Success: ${JSON.stringify(response.data)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
setText(`Failed: ${err}`);
}
return () => {
console.log("unmount");
};
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Abort</button>
</div>
);
}

Related

React custom http-hook abortController() bug

I have this custom http hook with abort when you try to go to a different page (I saw it in a tutorial but I am not truly sure I need it). When I fetch data with it and useEffect(), I have this error on the backend but the request is executed and everything is as planned. My question is, how to improve my code so it does not throw this error and do I need this functionality with abortController() ?
http-hook.ts
import { useCallback, useRef, useEffect } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { selectError, showError } from "src/redux/error";
import { selectLoading, startLoading, stopLoading } from "src/redux/loading";
export const useHttpClient = () => {
const dispatch = useDispatch();
const error = useSelector(selectError);
const loading = useSelector(selectLoading);
const activeHttpRequests: any = useRef([]);
const sendRequest = useCallback(
async (url, method = "GET", body = null, headers = {}) => {
dispatch(startLoading());
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) {
throw new Error(responseData.message);
}
dispatch(stopLoading());
return responseData;
} catch (err) {
dispatch(showError(err.message));
dispatch(stopLoading());
throw err;
}
},
[]
);
useEffect(() => {
return () => {
activeHttpRequests.current.forEach((abortCtrl: any) => abortCtrl.abort());
};
}, []);
return { loading, error, sendRequest };
};
UserInfo.tsx
import React, { Fragment, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useHttpClient } from "src/hooks/http-hook";
import classes from "./UserInfo.module.css";
const UserInfo = () => {
const { sendRequest } = useHttpClient();
const [currentUser, setCurrentUser] = useState<any>();
const userId = useParams<any>().userId;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const responseData = await sendRequest(
`http://localhost:5000/api/user/${userId}`
);
setCurrentUser(responseData.user);
console.log("currentUser ", currentUser);
} catch (err) {
console.log(err);
}
};
fetchCurrentUser();
}, [sendRequest ,userId]);
return currentUser ? (
<Fragment>
<div className={classes.cover} />
<div className={classes.user_info}>
<img
alt="user_img"
src={`http://localhost:5000/${currentUser.image}`}
className={classes.user_img}
/>
<div className={classes.text}>
<p>
Name: {currentUser.name} {currentUser.surname}
</p>
<p>Email: {currentUser.email}</p>
<p>Age: {currentUser.age}</p>
</div>
</div>{" "}
</Fragment>
) : (
<p>No current user</p>
);
};
export default UserInfo;
Backend
getCurrentUser.ts controller
const getCurrentUser = async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
const userId = req.params.userId;
let user;
try {
user = await User.findById(userId);
} catch (err) {
const error = new HttpError("Could not fetch user", 500);
return next(error);
}
res.json({ user: user.toObject({ getters: true }) });
};

How to make silent token refresh with axios interceptors in React using ContextAPI?

So what I'm trying to do is to refresh access token stored in ContextAPI by refresh token stored in a cookie. I use axios response interceptor to check 401 & 403 status codes and then call refresh function from useGetNewAccessToken hook which sends new access token from the server. The problem is, React app always keeps pushing me back to the login page on page refresh instead of calling this function and adding newly acquired access token to the Authorization header. Here is how I set my interceptors:
import axios from "axios";
import useGetNewAccessToken from "./useGetNewAccessToken";
import { AuthContext } from "./useAuth";
import { useContext, useEffect } from "react";
export const instance = axios.create({
baseURL: "http://localhost:8080",
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
export const useSetInterceptors = () => {
const { accessToken } = useContext(AuthContext);
const refresh = useGetNewAccessToken();
useEffect(() => {
const requestInterceptor = instance.interceptors.request.use(
(config) => {
if (!config.headers["Authorization"]) {
console.log("No authorization header is present");
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
const responseInterceptor = instance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const prevRequest = error?.config;
console.log("Request failed");
if (
error?.response?.status === 401 &&
error?.response?.status === 403 &&
!prevRequest.sent
) {
prevRequest.sent = true;
const accessToken = await refresh();
prevRequest.headers["Authorization"] = `Bearer ${accessToken}`;
return instance(prevRequest);
}
return Promise.reject(error);
}
);
return () => {
instance.interceptors.request.eject(requestInterceptor);
instance.interceptors.request.eject(responseInterceptor);
};
}, [accessToken, refresh]);
return instance;
};
export default useSetInterceptors;
I handle authentication process in a custom useAuth hook:
import React, { useState, createContext, useContext, useEffect } from "react";
import axios from "axios";
import { instance } from "../hooks/useSetInterceptors";
import { Link, useNavigate, useLocation } from "react-router-dom";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [authed, setAuthed] = useState(false);
const [moderator, setModerator] = useState(false);
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [authorities, setAuthorities] = useState([]);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/";
const signIn = async (e, username, password) => {
e.preventDefault();
const result = await getTokens(username, password);
if (result) {
console.log("User has signed in");
}
};
const getTokens = async (username, password) => {
const api = `http://localhost:8080/api/v1/public/signIn?username=${username}&password=${password}`;
const res = await instance.get(api, {
withCredentials: true,
params: {
username: username,
password: password,
},
});
const data = await res.data;
setAccessToken(data["access_token"]);
navigate(from, { replace: true });
};
return (
<AuthContext.Provider
value={{
authed,
setAuthed,
moderator,
setModerator,
getTokens,
getAccessTokenAuthorities,
username,
password,
setUsername,
setPassword,
signIn,
isModerator,
accessToken,
setAccessToken,
refreshToken,
setRefreshToken,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
This is the hook to send new access token by refresh token:
import axios from "axios";
import { AuthContext } from "./useAuth";
import { useContext } from "react";
export const useGetNewAccessToken = async (props) => {
const { accessToken, setAccessToken } = useContext(AuthContext);
const api = `http://localhost:8080/api/v1/public/getNewAccessToken`;
const refresh = async () => {
try {
const refreshToken = document.cookie.slice(14);
const res = await axios.get(api, {
withCredentials: true,
headers: {
Authorization: `Bearer ${refreshToken}`,
"Access-Control-Allow-Origin": "localhost:8080",
},
});
const data = await res.data;
setAccessToken(data["access_token"]);
return res.data["access_token"];
} catch (error) {
console.log(error);
}
};
return refresh();
};
export default useGetNewAccessToken;
Finally this is how I use interceptors inside my Inventory component:
import React, { useContext, useEffect, useState } from "react";
import useSetInterceptors from "../hooks/useSetInterceptors";
import { AuthContext } from "../hooks/useAuth";
import { useNavigate, useLocation } from "react-router-dom";
const Inventory = (props) => {
const [inventoryItems, setInventoryItems] = useState([]);
const { moderator, accessToken } = useContext(AuthContext);
const setInterceptors = useSetInterceptors();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const getInventoryItems = async () => {
const api = `http://localhost:8080/api/v1/moderator/inventory`;
try {
const res = await setInterceptors.get(api, {
withCredentials: true,
});
const data = await res.data;
console.log(data);
setInventoryItems(data);
} catch (err) {
console.log("Request failed..." + err);
navigate("/signIn", { state: { from: location }, replace: true });
}
};
getInventoryItems();
}, []);
return (
<div>
<h1>Inventory</h1>
</div>
);
};
Inventory.propTypes = {};
export default Inventory;
Can it have anything to do with the fact that interceptors call themselves multiple times instead of one even though I check that in response interceptor if statement and eject them after usage? Calls are shown in here

Not able to convert response data into JSON

I'm able to get response from API, but not able to convert response into Json and not able to return the data. It simply return null.
const responseData = async () => {
try{
const response = await axios.get('https://randomuser.me/api')
console.log(response) // console object
const jsonData = await response.json()
return jsonData;
}catch(err){
console.error(err)
}
}
export default function App() {
const [randomUserDataJson,setRandomUserDataJson] = useState('')
useEffect( () => {
responseData().then(randomdata => {
setRandomUserDataJson(randomdata || 'not found')
})
}, []);
return (
<div >
<pre>
<p>{randomUserDataJson}</p>
</pre>
</div>
);
}
Output
not found
You can directly return the axios response nothing but the promise and access the result using then method.
import axios from "axios";
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [randomUserDataJson, setRandomUserDataJson] = useState("");
useEffect(() => {
responseData().then((randomdata) => {
const data = JSON.stringify(randomdata.data);
setRandomUserDataJson(data || "not found");
});
}, []);
return (
<div>
<pre>
<p>{randomUserDataJson}</p>
</pre>
</div>
);
}
const responseData = async () => {
try {
const response = await axios.get("https://randomuser.me/api");
return response;
} catch (err) {
console.error(err);
}
};
codesandbox - https://codesandbox.io/s/withered-bush-iicqm?file=/src/App.js
You don't have to do const jsonData = await response.json(), axios will deserialize the response to JS Object for you. Just remove that line and it would work. Also, you can't render JS object as a child of a React Component, so it has to be stringified.
import axios from 'axios';
import { useState, useEffect } from 'react';
const responseData = async () => {
try{
const response = await axios.get('https://randomuser.me/api')
console.log(response) // console object
return response;
}catch(err){
console.error(err)
}
}
export default function App() {
const [randomUserDataJson,setRandomUserDataJson] = useState('')
useEffect( () => {
responseData().then(randomdata => {
setRandomUserDataJson(randomdata || 'not found')
})
}, []);
return (
<div >
<pre>
<p>{JSON.stringify(randomUserDataJson, null, 2)}</p>
</pre>
</div>
);
}

access token from auth0provider outside of react components

I'm using the auth0 token provided by the user on login to make api calls via useAuth0.getTokenSilently.
In this example, fetchTodoList, addTodoItem, and updateTodoItem all require a token for authorization. I'd like to be able to extract these functions out in to a separate file (like utils/api-client.js and import them without having to explicitly pass in the token.
import React, { useContext } from 'react'
import { Link, useParams } from 'react-router-dom'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faCircle, faList } from '#fortawesome/free-solid-svg-icons'
import axios from 'axios'
import { queryCache, useMutation, useQuery } from 'react-query'
import { TodoItem } from '../models/TodoItem'
import { TodoInput } from './TodoInput'
import { TodoList as TodoListComponent } from './TodoList'
import { TodoListsContext } from '../store/todolists'
import { TodoListName } from './TodoListName'
import { TodoList } from '../models/TodoList'
import { useAuth0 } from '../utils/react-auth0-wrapper'
export const EditTodoList = () => {
const { getTokenSilently } = useAuth0()
const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
try {
const token = await getTokenSilently!()
const { data } = await axios.get(
`/api/TodoLists/${todoListId}`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
)
return data
} catch (error) {
return error
}
}
const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
try {
const token = await getTokenSilently!()
const { data } = await axios.post(
'/api/TodoItems',
todoItem,
{
headers: {
Authorization: `Bearer ${token}`,
}
}
)
return data
} catch (addTodoListError) {
return addTodoListError
}
}
const updateTodoItem = async (todoItem: TodoItem) => {
try {
const token = await getTokenSilently!()
const { data } = await axios.put(
'/api/TodoItems',
todoItem,
{
headers: {
Authorization: `Bearer ${token}`,
}
}
)
return data
} catch (addTodoListError) {
return addTodoListError
}
}
const [updateTodoItemMutation] = useMutation(updateTodoItem, {
onSuccess: () => {
queryCache.refetchQueries(['todoList', todoListId])
}
})
const [addTodoItemMutation] = useMutation(addTodoItem, {
onSuccess: () => {
console.log('success')
queryCache.refetchQueries(['todoList', todoListId])
}
})
const onAddTodoItem = async (todoItem: TodoItem) => {
try {
await addTodoItemMutation({
...todoItem,
todoListId: parseInt(todoListId, 10)
})
} catch (error) {
// Uh oh, something went wrong
}
}
const { todoListId } = useParams()
const { status, data: todoList, error } = useQuery(['todoList', todoListId], () => fetchTodoList(todoListId))
const { todoLists, setTodoList } = useContext(TodoListsContext)
const todoListIndex = todoLists.findIndex(
list => todoListId === list.id.toString()
)
const setTodoItems = (todoItems: TodoItem[]) => {
// if(todoList) {
// const list = { ...todoList, todoItems }
// setTodoList(todoListIndex, list)
// }
}
const setTodoListName = (name: string) => {
// setTodoList(todoListIndex, { ...todoList, name })
}
return (
<>
<Link className="block flex align-items-center mt-8" to="/">
<span className="fa-layers fa-fw fa-3x block m-auto group">
<FontAwesomeIcon
icon={faCircle}
className="text-teal-500 transition-all duration-200 ease-in-out group-hover:text-teal-600"
/>
<FontAwesomeIcon icon={faList} inverse transform="shrink-8" />
</span>
</Link>
{status === 'success' && !!todoList && (
<>
<TodoListName
todoListName={todoList.name}
setTodoListName={setTodoListName}
/>
<TodoInput
onAddTodoItem={onAddTodoItem}
/>
<TodoListComponent
todoItems={todoList.todoItems}
setTodoItems={setTodoItems}
updateTodo={updateTodoItemMutation}
/>
</>
)}
</>
)
}
Here's a link to the repo: https://github.com/gpspake/todo-client
I was having a similar issue on how to use getAccessTokenSilently outside of a React component, what I ended up with was this:
My HTTP client wrapper
export class HttpClient {
constructor() {
HttpClient.instance = axios.create({ baseURL: process.env.API_BASE_URL });
HttpClient.instance.interceptors.request.use(
async config => {
const token = await this.getToken();
return {
...config,
headers: { ...config.headers, Authorization: `Bearer ${token}` },
};
},
error => {
Promise.reject(error);
},
);
return this;
}
setTokenGenerator(tokenGenerator) {
this.tokenGenerator = tokenGenerator;
return this;
}
getToken() {
return this.tokenGenerator();
}
}
On my App root, I pass the getAccessTokenSilently from auth0
useEffect(() => {
httpClient.setTokenGenerator(getAccessTokenSilently);
}, [getAccessTokenSilently]);
And that's it!
You now have an axios instance ready to do authenticated requests with
This is a variant of the answer by #james-quick, where I am using a "RequestFactory" to generate requests in the axios format, and then just adding the auth header from Auth0
I was facing the same issue, and I got around this limitation by moving all my API call logic into a custom hook that I created:
import { useAuth0 } from '#auth0/auth0-react';
import { useCallback } from 'react';
import makeRequest from './axios';
export const useRequest = () => {
const { getAccessTokenSilently } = useAuth0();
// memoized the function, as otherwise if the hook is used inside a useEffect, it will lead to an infinite loop
const memoizedFn = useCallback(
async (request) => {
const accessToken = await getAccessTokenSilently({ audience: AUDIANCE })
return makeRequest({
...request,
headers: {
...request.headers,
// Add the Authorization header to the existing headers
Authorization: `Bearer ${accessToken}`,
},
});
},
[isAuthenticated, getAccessTokenSilently]
);
return {
requestMaker: memoizedFn,
};
};
export default useRequest;
Usage Example:
import { RequestFactory } from 'api/requestFactory';
const MyAwesomeComponent = () => {
const { requestMaker } = useRequest(); // Custom Hook
...
requestMaker(QueueRequestFactory.create(queueName))
.then((response) => {
// Handle response here
...
});
}
RequestFactory defines and generates the request payload for my different API calls, for example:
export const create = (queueName) => ({ method: 'post', url: '/queue', data: { queueName } });
Here is a full Auth0 integration PR for reference.
I'm not exactly sure why you couldn't access the token inside of your individual functions? Is it because they wouldn't be React function components but just regular functions?
One of the things I have done is create a useFetch hook that can get the user token and attach it to a request itself. Then, instead of exporting those functions specifically,I can just call this new fetch hook. Here's an example of what I mean.
import React from "react"
import { useAuth0 } from "../utils/auth"
const useFetch = () => {
const [response, setResponse] = React.useState(null)
const [error, setError] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const { getTokenSilently } = useAuth0()
const fetchData = async (url, method, body, authenticated, options = {}) => {
setIsLoading(true)
try {
if (authenticated) {
const token = await getTokenSilently()
if (!options.headers) {
options.headers = {}
}
options.headers["Authorization"] = `Bearer ${token}`
}
options.method = method
if (method !== "GET") {
options.body = JSON.stringify(body)
}
const res = await fetch(url, options)
const json = await res.json()
setResponse(json)
setIsLoading(false)
if (res.status === 200) {
return json
}
throw { msg: json.msg }
} catch (error) {
console.error(error)
setError(error)
throw error
}
}
return { response, error, isLoading, fetchData }
}
export default useFetch
Ok, got it!
Now that I understand better, my real question was how to provide an auth0 token to axios requests such that they don't need to be declared in components.
The short answer:
Get the token when auth0 is initialized and register an axios interceptor to set that token as a header value for all axios requests.
The long answer (examples in typescript):
Declare a function that takes a token and registers an axios interceptor
const setAxiosTokenInterceptor = async (accessToken: string): Promise<void> => {
axios.interceptors.request.use(async config => {
const requestConfig = config
if (accessToken) {
requestConfig.headers.common.Authorization = `Bearer ${accessToken}`
}
return requestConfig
})
}
In the auth0provider wrapper, when the auth0 client is initialized and authenticated, get the token with setAxiosTokenInterceptor and pass it to the function that registers the interceptor (Modified example from the Auth0 React SDK Quickstart):
useEffect(() => {
const initAuth0 = async () => {
const auth0FromHook = await createAuth0Client(initOptions)
setAuth0(auth0FromHook)
if (window.location.search.includes('code=')) {
const { appState } = await auth0FromHook.handleRedirectCallback()
onRedirectCallback(appState)
}
auth0FromHook.isAuthenticated().then(
async authenticated => {
setIsAuthenticated(authenticated)
if (authenticated) {
auth0FromHook.getUser().then(
auth0User => {
setUser(auth0User)
}
)
// get token and register interceptor
const token = await auth0FromHook.getTokenSilently()
setAxiosTokenInterceptor(token).then(
() => {setLoading(false)}
)
}
}
)
}
initAuth0().catch()
}, [])
Calling setLoading(false) when the promise is resolved ensures that, if auth0 is finished loading, the interceptor has been registered. Since none of the components that make requests are rendered until auth0 is finished loading, this prevents any calls from being made without the token.
This allowed me to move all of my axios functions in to a separate file and import them in to the components need them. When any of these functions are called, the interceptor will add the token to the header
utils/todo-client.ts
import axios from 'axios'
import { TodoList } from '../models/TodoList'
import { TodoItem } from '../models/TodoItem'
export const fetchTodoLists = async (): Promise<TodoList[]> => {
try {
const { data } = await axios.get(
'/api/TodoLists'
)
return data
} catch (error) {
return error
}
}
export const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
try {
const { data } = await axios.get(
`/api/TodoLists/${todoListId}`
)
return data
} catch (error) {
return error
}
}
export const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
try {
const { data } = await axios.post(
'/api/TodoItems',
todoItem
)
return data
} catch (addTodoListError) {
return addTodoListError
}
}
...
Full source on github
I like to have my API calls in their own directory (under /api for example) and have the code to call to the API be as small as possible. I took a similar approach to others on here, using Auth0, TypeScript, Axios (including an interceptor) and React hooks.
The TLDR answer
Place your Axios interceptor within a hook, and then use that hook within segmented API hooks (ie, useUserApi, useArticleApi, useCommentApi and so on). You can then cleanly call your API using Auth0.
The long answer
Define your Axios hook, I've only covered the HTTP methods I'm currently using:
# src/api/useAxios.ts
import { useAuth0 } from '#auth0/auth0-react';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
// We wrap Axios methods in a hook, so we can centrally handle adding auth tokens.
const useAxios = () => {
const { getAccessTokenSilently } = useAuth0();
axios.interceptors.request.use(async (config: any) => {
if (config.url.indexOf('http') === -1) {
config.url = `${process.env.REACT_APP_API_ENDPOINT}/${config.url}`;
}
if (typeof config.headers.Authorization === 'undefined') {
config.headers.Authorization = `Bearer ${await getAccessTokenSilently()}`;
}
return config;
});
return {
get: async (url: string, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.get(url, config),
delete: async (url: string, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.delete(url, config),
post: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.post(url, data, config),
put: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.put(url, data, config),
patch: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.patch(url, data, config),
}
};
export default useAxios;
What I'm doing here is adding a bearer token by calling getAccessTokensSilently() if one is not already defined. Additionally, if HTTP isn't present in my URL then I append the default API URL from my environment variables - this means I can keep my request short and not have to use the full URL every time.
Now I define a hook based on my user API as following:
# src/api/useUserApi.ts
import { UserInterface } from '[REDACTED]/types';
import { AxiosResponse } from 'axios';
import useAxios from './useAxios';
const useUserApi = () => {
const { get, post, put } = useAxios();
return {
getUser: (id: string): Promise<AxiosResponse<UserInterface>> => get(`user/${id}`),
putUser: (user: UserInterface) => put('user', user),
postUser: (user: UserInterface) => post('user', user),
}
};
export default useUserApi;
You can see I expose the underlying HTTP methods from Axios, and then use them in API specific scenarios, getUser, putUser, and postUser.
Now I can go ahead and call my API in some application logic, keeping the API code to the absolute minimum but still allowing full pass through and typing of the Axios objects.
import { useAuth0 } from '#auth0/auth0-react';
import { useNavigate } from 'react-router';
import useUserApi from '../../../api/useUserApi';
const LoginCallback = (): JSX.Element => {
const navigate = useNavigate()
const { user, isAuthenticated, isLoading } = useAuth0();
const { getUser, putUser, postUser} = useUserApi();
const saveUserToApi = async () => {
if (!user?.sub) {
throw new Error ('User does not have a sub');
}
// Try and find the user, if 404 then create a new one for this Auth0 sub
try {
const userResult = await getUser(user.sub);
putUser(Object.assign(user, userResult.data));
navigate('/');
} catch (e: any) {
if (e.response.status === 404) {
postUser({
id: user.sub,
email: user.email,
name: user.name,
givenName: user.givenName,
familyName: user.familyName,
locale: user.locale,
});
navigate('/');
}
}
}
if (isLoading) {
return <div>Logging you in...</div>;
}
if (isAuthenticated && user?.sub) {
saveUserToApi();
return <p>Saved</p>
} else {
return <p>An error occured whilst logging in.</p>;
}
};
export default LoginCallback;
You can note the above postUser, putUser and getUser API requests are all one liners, with only the declaration (const { getUser, putUser, postUser} = useUserApi();) and import being required otherwise.
This answer is definitely standing on the shoulders of giants, but I thought it would be useful all the same to someone who likes to keep their API calls as clean as possible.
There are different ways to solve this.
To not change your code base too much. I would go with a store with a provider and a hook. There are many store libraries out there.
Here is a tiny version which also can be used outside React rendering.
https://github.com/storeon/storeon
This was just one example of a very small store I could find that might fit the bill.
Using a store library outside React could look like:
import store from './path/to/my/store.js;'
// Read data
const state = store.get();
// Save data in the store
store.dispatch('foo/bar', myToken);

Check if dispatch action is called in axios interceptor using Jest

I want to test if the loginReset() function is being called every time there's an unauthorized request or response status code 401.
My code is what follows:
use-request.js
import axios from "axios"
import { axiosDefaultOptions } from "../config"
import { useSelector, useDispatch } from "react-redux"
import { loginReset } from "../store/reducers/login-slice"
const useRequest = (auth=false) => {
const request = axios.create(axiosDefaultOptions)
const dispatch = useDispatch()
if(auth){
const token = useSelector( state => state.login.data ? state.login.data.accessToken : null )
request.interceptors.request.use(config => {
config.headers.Authorization = token ? `Bearer ${token}` : ''
return config
})
request.interceptors.response.use(response => {
return response
}, error => {
if(error.response.status === 401) {
dispatch(loginReset())
}
return Promise.reject(error)
})
}
return request
}
export default useRequest
use-request.test.js
import { testHookwithStore } from "../utils"
import faker from "faker"
import { useRequest } from "../../components/hooks"
import configureStore from "redux-mock-store"
import MockAdapter from "axios-mock-adapter"
import { axiosDefaultOptions } from "../../components/config"
import thunk from "redux-thunk"
describe("useRequest", () => {
faker.seed(123);
let request = null
let authRequest = null
let token = faker.random.uuid()
const mockStore = configureStore([thunk])
let authRequestAdapter = null
const fakeDomainWord = faker.internet.domainWord()
const fakeUrl = `${axiosDefaultOptions.baseURL}/${fakeDomainWord}`
beforeEach(() => {
let store = mockStore({
login: { data: { accessToken: token } }
})
testHookwithStore(store, () => {
request = useRequest()
authRequest = useRequest(true)
authRequestAdapter = new MockAdapter(authRequest)
authRequestAdapter.onPost(fakeDomainWord, {}).reply(401, { code: 401, message: "Bad credentials" })
})
})
test("Request should have no headers", () => {
request.interceptors.request.use( config => {
expect(config.headers.Authorization).toBeNull()
})
})
test("Auth request should have Authentication Headers", () => {
authRequest.interceptors.request.use( config => {
expect(config.headers.Authorization).toBe(`Bearer ${token}`)
})
})
test("Auth request resets login when 401", async () => {
const loginReset = jest.fn()
try{
await authRequest.post(fakeUrl, {})
}
catch(error){
expect(loginReset).toHaveBeenCalledTimes(1)
}
})
})
testHookwithStore basically just creates a component wrapped around a provider. The last test is failing and I'm not sure how I would verify if the dispatch is actually working. Any clues here?
Apparently, there's a getActions() function on the mocked store.
test("Auth request resets login when 401", async () => {
try{
await authRequest.post(fakeUrl, {})
}
catch(error){
expect(store.getActions()[0].type).toBe("loginReset")
}
})

Resources