♾️ React, Redux, Auth state and API

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";constHTTP_REJECTED = 'HTTP_REJECTED';constsuppressedTypes = [HTTP_REJECTED];// this middleware suppresses uncaught rejections for HTTP actions consterrorMiddleware = () => 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) => { letstore; // this is some library that makes requests constapi = ({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() ) ); returnstore;};
Copy
JavaScript
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} }) });
Copy
JavaScript
In reducer you can now also invalidate token on HTTP 401 errors:
import {combineReducers} from "redux";constisHttpAuthError = (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 });
Copy
JavaScript
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.