React + Fetch - Logout on 401 Unauthorized or 403 Forbidden HTTP Response
This is a quick example of how to automatically logout of a React app if a fetch request returns a 401 Unauthorized
or 403 Forbidden
response.
The code snippets in this tutorial are from a React + Recoil Login tutorial I posted recently, to see the code running in a live demo app check out React + Recoil - User Registration and Login Example & Tutorial.
Recoil is used in the example to store the current authenticated user in the auth
shared state object, but Recoil isn't required if your React app uses another way to store the user's logged in state such as Redux or RxJS etc, the only requirement is that you can get and set the user's logged in state.
Fetch Wrapper with Logout on 401 or 403
The fetch wrapper is a lightweight wrapper around the native browser fetch()
function used to simplify the code for making HTTP requests by automatically handling request errors, parsing JSON response data and setting the HTTP auth header. It returns an object with methods for making get
, post
, put
and delete
requests.
The handleResponse()
function checks if there is an HTTP error in the response (!response.ok
), if there is an error and the response status code (response.status
) is 401
or 403
the user is logged out of the React app and redirected to the login page.
With the fetch wrapper a POST
request can be made as simply as this: fetchWrapper.post(url, body);
. It's called in the example app by user actions.
import { useRecoilState } from 'recoil';
import { history } from '_helpers';
import { authAtom } from '_state';
import { useAlertActions } from '_actions';
export { useFetchWrapper };
function useFetchWrapper() {
const [auth, setAuth] = useRecoilState(authAtom);
const alertActions = useAlertActions();
return {
get: request('GET'),
post: request('POST'),
put: request('PUT'),
delete: request('DELETE')
};
function request(method) {
return (url, body) => {
const requestOptions = {
method,
headers: authHeader(url)
};
if (body) {
requestOptions.headers['Content-Type'] = 'application/json';
requestOptions.body = JSON.stringify(body);
}
return fetch(url, requestOptions).then(handleResponse);
}
}
// helper functions
function authHeader(url) {
// return auth header with jwt if user is logged in and request is to the api url
const token = auth?.token;
const isLoggedIn = !!token;
const isApiUrl = url.startsWith(process.env.REACT_APP_API_URL);
if (isLoggedIn && isApiUrl) {
return { Authorization: `Bearer ${token}` };
} else {
return {};
}
}
function handleResponse(response) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if ([401, 403].includes(response.status) && auth?.token) {
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
localStorage.removeItem('user');
setAuth(null);
history.push('/account/login');
}
const error = (data && data.message) || response.statusText;
alertActions.error(error);
return Promise.reject(error);
}
return data;
});
}
}
User Actions
The user actions object returned by the useUserActions()
hook function contains methods for user registration, authentication and CRUD operations. It handles communication between the React app and the backend api for everything related to users, and also handles Recoil state update operations for users and auth atoms. HTTP requests to the API are sent with the fetch wrapper.
I included it here to show examples of the fetchWrapper
being called to make HTTP requests to the API from the React app.
import { useRecoilState, useSetRecoilState, useResetRecoilState } from 'recoil';
import { history, useFetchWrapper } from '_helpers';
import { authAtom, usersAtom, userAtom } from '_state';
export { useUserActions };
function useUserActions () {
const baseUrl = `${process.env.REACT_APP_API_URL}/users`;
const fetchWrapper = useFetchWrapper();
const [auth, setAuth] = useRecoilState(authAtom);
const setUsers = useSetRecoilState(usersAtom);
const setUser = useSetRecoilState(userAtom);
return {
login,
logout,
register,
getAll,
getById,
update,
delete: _delete,
resetUsers: useResetRecoilState(usersAtom),
resetUser: useResetRecoilState(userAtom)
}
function login({ username, password }) {
return fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
.then(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
setAuth(user);
// get return url from location state or default to home page
const { from } = history.location.state || { from: { pathname: '/' } };
history.push(from);
});
}
function logout() {
// remove user from local storage, set auth state to null and redirect to login page
localStorage.removeItem('user');
setAuth(null);
history.push('/account/login');
}
function register(user) {
return fetchWrapper.post(`${baseUrl}/register`, user);
}
function getAll() {
return fetchWrapper.get(baseUrl).then(setUsers);
}
function getById(id) {
return fetchWrapper.get(`${baseUrl}/${id}`).then(setUser);
}
function update(id, params) {
return fetchWrapper.put(`${baseUrl}/${id}`, params)
.then(x => {
// update stored user if the logged in user updated their own record
if (id === auth?.id) {
// update local storage
const user = { ...auth, ...params };
localStorage.setItem('user', JSON.stringify(user));
// update auth user in recoil state
setAuth(user);
}
return x;
});
}
// prefixed with underscored because delete is a reserved word in javascript
function _delete(id) {
setUsers(users => users.map(x => {
// add isDeleting prop to user being deleted
if (x.id === id)
return { ...x, isDeleting: true };
return x;
}));
return fetchWrapper.delete(`${baseUrl}/${id}`)
.then(() => {
// remove user from list after deleting
setUsers(users => users.filter(x => x.id !== id));
// auto logout if the logged in user deleted their own record
if (id === auth?.id) {
logout();
}
});
}
}
Need Some React Help?
Search fiverr for freelance React developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!