React, Redux, Auth state and API

Kirill Konshin
D.i.S
Published in
2 min readApr 20, 2018

One of the most frequently asked questions about React, Redux and REST is where to put authentication (OAuth tokens for example), how to wire it to Redux and how to add tokens to API calls.

Since user auth is a critical part of app state it makes sense to put it into Redux store. I suggest to add an API call method to Redux by utilizing the redux-thunk middleware’s functionality and dispatch Promise from API actions by using redux-promise-middleware. Let’s begin with store configuration:

import {applyMiddleware, createStore} from "redux";
import thunkMiddleware from "redux-thunk";
import promiseMiddleware from "redux-promise-middleware";
import isPromise from "is-promise";
import {request} from "./lib";
import reducers from "./reducers";
const HTTP_REJECTED = 'HTTP_REJECTED';const suppressedTypes = [HTTP_REJECTED];// this middleware suppresses uncaught rejections for HTTP actions
const errorMiddleware = () => next => action => {

if (!isPromise(action.payload)) return next(action);

if (suppressedTypes.includes(action.type)) {
// Dispatch initial pending promise, but catch any errors
return next(action).catch(error => {
console.warn('Middleware has caught an error', error);
return error;
});
}
return next(action);};export default (initialState) => { let store;

// this is some library that makes requests
const api = ({method = 'GET', url, data, query}) => request({
method,
url,
data,
query,
token: store.getState().token // here goes your selector
}).catch(e => {
// service action for reducers to capture HTTP errors
store.dispatch({
type: HTTP_REJECTED,
payload: e
});
throw e; // re-throw an error }); store = createStore(
reducers,
initialState,
applyMiddleware(
thunkMiddleware.withExtraArgument(api),
errorMiddleware, // must be before promiseMiddleware
promiseMiddleware()
)
);
return store;};

Note that we first initialize empty store that we will access later in api function because we need api function as parameter for thunkMiddleware.withExtraArgument which is used for store creation. This can be done in a more elegant way, of course, but for simplicity we do it this brutal way.

Then you can dispatch a Promise returned from API method that becomes available in actions (redux-thunk makes it possible):

export const loadUser = (id) => (dispatch, getState, api) => dispatch({
type: 'LOAD_USER',
payload: api({url: `/users/${id}`})
});
export const login = (username, password) =>
(dispatch, getState, api) => dispatch({
type: 'LOGIN',
payload: api({
method: 'POST',
url: `/login`,
body: {username, password}
})
});

In reducer you can now also invalidate token on HTTP 401 errors:

import {combineReducers} from "redux";const isHttpAuthError = (payload) => (
// make sure request library error has response in it
payload && payload.response && payload.response.status === 401
);
const token = (state = null, {type, payload}) => {
switch (type) {
case ('LOGIN_FULLFILLED'):
// here you get a token from login API response
return payload.token;
case (HTTP_REJECTED):
return (isHttpAuthError(payload)) ? null : state;
case ('LOGIN_REJECTED'):
case ('LOGOUT_FULLFILLED'):
// here you reset the token
return null;
default:
return state;
}
};
export default combineReducers({
token
});

This all makes your app state to be always consistent, you can capture network auth-related errors in the same place where you take care of other token-related processes.

This repo https://github.com/kirill-konshin/react-redux-router-auth contains all above mentioned concepts and a bit more.

--

--

Kirill Konshin
D.i.S
Editor for

Staff Software Developer @ RingCentral, Inc., Music Maker, Traveller, Photographer