♾️ React, Redux, Auth state and API2018-04-20
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.