Resolving the store api dependency

/ Lars

In frontend projects with a redux store, all requests need to be authenticated. But how do you put the token into the store, but still have it in the API module?

In a few recent projects I ran into a problem with initialisation sequence in the frontend, with regards to api authentication.

In all of these, there was a redux store which maintains shared (semi-)global state. In that state, we also have the user object and the user authentication token.

In order to make requests there was an explicit API module. In some apollo as a graphql-client, in others axios or just plain fetch wrapped together with a list of known endpoints and their respective arguments. That API module is tasked with filling in the current token, if there is any, in order to authenticate the requests.

And to bind the store and the API together, we used redux-thunk with an additional argument.

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';

import { createApi } from './api';
import reducer from './reducer';

export default function initStore() {
  const api = createApi(store);
  const store = createStore(
    reducer,
    applyMiddleware(thunk.withExtraArgument(api))
  );
  return store;
}

There is a problem though: The store variable is used before it is valid. The API needs to know about the store in order to fetch the token. But in order to get a store, we need to hand it an API module.

With such a structured explanation to the problem, the solution I’m now happy with is almost self-evident: pass a reference to a future store to the api module, with the caveat that the store can not be used during the initialization of the API object.

/* … */

export default function initStore() {
  let store; // resolve circular dependency between api and store
  const api = createApi(store);
  store = createStore(
    reducer,
    applyMiddleware(thunk.withExtraArgument(api))
  );
  return store;
}

And for good measure, decouple the api from the store internals altogether:

/* … */
import { getToken } from './user.selectors';

export default function initStore() {
  let store; // resolve circular dependency between api and store
  const api = createApi(() => getToken(store.getState()));
  store = createStore(
    reducer,
    applyMiddleware(thunk.withExtraArgument(api))
  );
  return store;
}

Alternatives

In the wild, I’ve seen some other approaches to this problem. Every project comes with unique constraints.

In most situations, you want to keep the authentication credentials across page refreshes. For that, they’re stored in a truly global place where they can be read from during initialization, usually localstorage. Instead of relying on the store, the API module may just access this place directly. This scheme feels a bit smelly, because the API module relies on sequencing-guarantees of the potentially-asynchronous state-handling library.

Then you can resolve this by not giving access to the API module via store middleware, but some other mechanism that is initialized after the store. For example a second HOC, which receives the store as argument – explicitly or using the store-context. These schemes tend to be more involved. Discussing such patterns and their trade-offs is out of the scope of this article though.

Last but not least there are projects that don’t have this problem because the logged-in state can not change during the lifetime of the api and store. The token is available from the get-go (e.g. via cookie, DOM-injection, or URL params), and there will be no change in token before the page is refreshed. In those cases, you can initialize the API module with the token directly, and might not even need to have it in the store.

© 2020 bitcrowd GmbH.