What’s the Best Way to Store Tokens in Redux?

One problem I’ve been struggling with lately is: what is the best way to store tokens when using redux? I could store it in the Redux state, but then it wouldn’t be there after refresh. I could just store it in the browser’s localStorage, but where would I do that?

Before I dive into what I found to be the best way, let’s review the wrong ways.

The Wrong Ways to Store Your Token

Saving the Token in the Application State

This is probably the first method of saving the token that will come to a developer’s mind. It’s simple, and it makes sense when you’re neck-deep in redux. However, the state will only be there until the browser is closed or the window is refreshed.

This is bad because typically when you log into a website and refresh the page, you expect to retain your logged-in status. Therefore, storing the token in your application state is not a valid option.

Save the Token to LocalStorage in your Login Action

Saving the token to localStorage is one way to go. It would be really easy to read the get the value back from localStorage on application startup. But where would this logic go?

When I first thought of doing this, it made sense to me to do this in the actions. It’s where I make API requests, so why shouldn’t I put a simple call to localStorage here?

Well, you could do it this way, but it kind of goes outside the scope of what actions are supposed to do: produce an event for reducers to receive.

Save the Token to LocalStorage in the Reducers

You could also just save the token to localStorage in your reducer. This would also work, but it would be an abomination to reducers. Reducers are not supposed to have side-affects. They’re supposed to do exactly one thing: create a new state for the application.

The Correct Ways to Store Your Token

Now let’s look at the correct ways to store the token, and other data, to localStorage. I’ve found there to be only two good solutions to doing this.

Store your State to LocalStorage in a Subscriber

Dan Abramov, one of the co-authors of Redux, created a video on saving your state to localStorage using subscribers. I think this is a great solution for functionality where you’re simply taking the data from storage and putting it in your state.

To do this is pretty simple.

Create a subscriber that saves the state you want.

store.subscribe(() =>
  localStorage.setItem('TOKEN', store.getState().token);
});

Now the token will be saved to localStorage whenever the state is updated. To retrieve it from localStorage when the app starts, you just have to modify your initial state before creating the store.

const startState = {
  ...initialState,
  token: localStorage.getItem('TOKEN')
};

createStore(reducer, startState);

Now, keep in mind that this is a very simple example. Before using this in production, we should really create some helper functions to save/get the state from localStorage and catch errors that may occur, just like Dan does in his video linked above.

As I said, this is a great solution for simply grabbing a token from localStorage and saving it back to your state (and vice versa).

However, what if our token might be close to expiring? Should we verify or refresh our token when we get it? I probably would.

Store and Refresh your Token in Middleware

If I need to perform actions on a value after I get it from localStorage and before I put it in my application state, then I do it in the middleware. Middleware is the ideal place to perform asynchronous actions. To accomplish this, we’ll create a middleware that listens for an “ON_INIT” event, and the event where we receive our token, which I’ll just call “RECEIVE_TOKEN”.

The first step is to create a new middleware that listens for these events.

export const persistToken = store => next => action => {

  switch(action.type) {

    case ON_INIT:
      break;

    case RECEIVE_TOKEN:
      break;

  }

  return next(action);
};

Great, now we can add logic to save the token.

...
case RECEIVE_TOKEN:
  localStorage.setItem('TOKEN', action.token);
  break;
...

Assuming there is an action being dispatched after the token is received from the API which matches the format above, this will intercept that action before it makes it to the store and saves our token.

Then, we can read it back in the ON_INIT case.

...
case ON_INIT:
  //dispatch a new action with the token from localStorage
  next({
    type: RECEIVE_TOKEN,
    token: localStorage.getItem('TOKEN')
  });
  break;
...

Once again, I’ll remind you that this is meant to be a very simple example, and we should add some validation and error handling logic to this before using it in production.

So how do we verify our token before loading it?

We can do this by making a fetch to our authentication API, so long as it has an endpoint to verify or refresh tokens.

Making this request before saving the token can be done like so:

...
case ON_INIT:
  // verify the token first
  fetch('your.api.com/verify-token', {
    body: JSON.stringify({
      token: localStorage.getItem('TOKEN')
    })
  })
  .then((response) => {
    if(response.ok) {
      // if you also want to refresh your token, do it here
      // dispatch a new action with the token
      next({
        type: RECEIVE_TOKEN,
        token: localStorage.getItem('TOKEN')
      });
    }
  });
 break;
...

Now our code will verify that tokens are still valid before saving them to our application state. We can also refresh our tokens here, which I often find myself doing.

Conclusion

Something as simple as storing authentication tokens can become so complex in today’s world. While there are many ways to solve this problem, I like to find the ways that best fit into the architecture of the frameworks I’m using. That’s an important qualifier for a good solution in my book.

To sum it up, if you need to just grab some state from localStorage and shove it into your application state, I highly recommend using a subscriber. Conversely, if you need to perform asynchronous actions before saving that data to your application state, use middleware.

If you have another solution, or a new spin on one of these solutions, leave a comment below!