♾️ React, Redux, Auth state and API2018-04-20

♾️ 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';

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.