React auth with react-query and axios
I am going to try and show you a simple way to handle login and refresh-token state if you have react-query and axios in your arsenal.
This approach relies on that your backend returns the refresh-token in a same-site
http-only cookie when your users are logging into your application.
We will utilize the axios-hooks package to achieve this, our initial component will look something like this.
function AuthProvider(props: any) {
const accessTokenRef = React.useRef<string>();
const [tokenExpires, setTokenExpires] = React.useState<string>();
...
}
This is how we handle our critical state, now we need to make the user able to login, that can look like this:
const loginRequest = async (username: string, password: string) => {
const { token, ...otherDataYouMightNeed } = await yourBackendLoginRequest(
username,
password,
);
return { token, otherDataYouMightNeed };
};
const loginQuery = useMutation(loginRequest, {
onSuccess: (data) => {
// here we rely on the returned data contains the user, the token and its expiration date.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
});
const refreshRequest = async () => {
// it is important that you use axios when fetching the refresh-token, that way we know the cookie
// with the refresh-token is included
const { token, ...otherDataYouMightNeed } = await axios.get(
"/your-refresh-token-endpoint",
);
return { token, otherDataYouMightNeed };
};
// this request should not have to include any logic as we are sending the token value with the cookeis.
const refreshQuery = useMutation(refreshRequest, {
onSuccess: (data) => {
// the refresh-token request should return similiar data as the loginRequest.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
// here we set a refetch-interval to avoid us sending a request without a valid access token.
// you can either hardcode this value or calculate the diff until your token expires.
refetchInterval: 300000,
});
const login = async (username: string, password: string) => {
await loginQuery.mutateAsync(username, password);
// you might want to wrap this in try / catch to handle errors and alert the user
// if the username/password is incorrect.
};
Now we want to bind the access token to our axios config, submitting the credentials with every request to the backend.
useEffect(() => {
// add authorization token to each request
axios.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
config.baseURL = BASE_URL; // base url for your api.
config.headers.authorization = `Bearer ${accessTokenRef.current}`;
// withCredentials should be enabled to submit the cookies with each request.
// this is essential to refresh the access-token.
config.withCredentials = true;
return config;
},
);
axios.interceptors.response.use(
(response) => response,
async (error) => {
return Promise.reject(error);
},
);
// configure axios-hooks to use this instance of axios
configure({ axios });
}, []);
Now, wherever else you import axios around in the application, the config defined above will be used.
The entire component can look something like this:
const loginRequest = async (username: string, password: string) => {
const { token, ...otherDataYouMightNeed } = await yourBackendLoginRequest(
username,
password
);
return { token, otherDataYouMightNeed };
};
const refreshRequest = async () => {
// it is important that you use axios when fetching the refresh-token, that way we know the cookie
// with the refresh-token is included
const { token, ...otherDataYouMightNeed } = await axios.get(
"/your-refresh-token-endpoint"
);
return { token, otherDataYouMightNeed };
};
function AuthProvider(props: any) {
const accessTokenRef = React.useRef<string>();
const [tokenExpires, setTokenExpires] = React.useState<string>();
const loginQuery = useMutation(loginRequest, {
onSuccess: (data) => {
// here we rely on the returned data contains the user, the token and its expiration date.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
});
// this request should not have to include any logic as we are sending the token value with the cookies.
const refreshQuery = useMutation(refreshRequest, {
onSuccess: (data) => {
// the refresh-token request should return similiar data as the loginRequest.
accessTokenRef.current = data.token;
setTokenExpires(data.tokenExpires);
},
// here we set a refetch-interval to avoid us sending a request without a valid access token.
// you can either hardcode this value or calculate the diff until your token expires.
refetchInterval: 300000,
});
const login = async (username: string, password: string) => {
await loginQuery.mutateAsync(username, password);
// you might want to wrap this in try / catch to handle errors and alert the user
// if the username/password is incorrect.
};
useEffect(() => {
// add authorization token to each request
axios.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
config.baseURL = BASE_URL;
config.headers.authorization = `Bearer ${accessTokenRef.current}`;
// this is important to include the cookies when we are sending the requests to the backend.
config.withCredentials = true;
return config;
}
);
axios.interceptors.response.use(
(response) => response,
async (error) => {
return Promise.reject(error);
}
);
// configure axios-hooks to use this instance of axios
configure({ axios });
}, []);
const isSuccess = loginQuery.isSuccess || refetchQuery.isSuccess;
const isAuthenticated = isSuccess && !!accessTokenRef.current;
// if you need a user object you can do something like this.
const user = refetchQuery.data.user || loginQuery.data.user;
// example on provider
return (
<AuthContext.Provider
value={{
isAuthenticated,
user,
login,
logout,
}}
{...props}
></AuthContext.Provider>
);
}
Now you can create a hook where you can use the auth data.
const useAuth = () => {
const context = React.useContext(AuthContext);
if (context === undefined) {
throw new Error("AuthContext must be within AuthProvider");
}
return context;
};
Then you can do something like this around in your application (remember to include AppContext.Provider
somewhere in your app).
const { isAuthenitcated, user } = useAuth();
And there you have it, a very simple approach to handle authentication and requests with react-query and axios.
Thanks for reading.