Search code examples
reactjsreduxlocal-storageredux-toolkit

Local storage using redux toolkit


I would like to keep my isAuthenticated state in local storage, so after refreshing the page, the user will be logged in. I tried straightforward, to set it ti true/false in localStorage and to set initial value of my state in redux to this value, but it always sets it to true.

Here's my redux store

import { createSlice, configureStore } from '@reduxjs/toolkit';

//MOVEMENTS (doesn't work yet)
const initialMovementsState = {
  movements: [],
};

const movementsSlice = createSlice({
  name: 'movements',
  initialState: initialMovementsState,
  reducers: {
    add(state) {
      //nothing yet
    },
    decrement(state) {
      //nothing yet
    },
  },
});

//LOGGING IN/OUT
const initialAuthState = {
  isAuthenticated: false,
};

const authSlice = createSlice({
  name: 'auth',
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    },
  },
});

//STORE CONFIGURATION

const store = configureStore({
  reducer: {
    movements: movementsSlice.reducer,
    auth: authSlice.reducer,
  },
});

export const movementsActions = movementsSlice.actions;
export const authActions = authSlice.actions;

export default store;

All answers I found are with redux only, not with redux toolkit and I'm kinda fresh to redux, so I'm lost.


Solution

  • Update October 2022: You can also use redux-toolkit's createListenerMiddleware in versions 1.8 and up, as explained in this answer.


    Changing localStorage is a side-effect so you don't want to do it in your reducer. A reducer should always be free of side-effects. One way to handle this is with a custom middleware.

    Writing Middleware

    Our middleware gets called after every action is dispatched. If the action is login or logout then we will change the localStorage value. Otherwise we do nothing. Either way we pass the action off to the next middleware in the chain with return next(action).

    The only difference in the middleware between redux-toolkit and vanilla redux is how we detect the login and logout actions. With redux-toolkit the action creator functions include a helpful match() function that we can use rather than having to look at the type. We know that an action is a login action if login.match(action) is true. So our middleware might look like this:

    const authMiddleware = (store) => (next) => (action) => {
      if (authActions.login.match(action)) {
        // Note: localStorage expects a string
        localStorage.setItem('isAuthenticated', 'true');
      } else if (authActions.logout.match(action)) {
        localStorage.setItem('isAuthenticated', 'false');
      }
      return next(action);
    };
    

    Applying Middleware

    You will add the middleware to your store in the configureStore function. Redux-toolkit includes some middleware by default with enables thunk, immutability checks, and serializability checks. Right now you are not setting the middleware property on your store at all, so you are getting all of the defaults included. We want to make sure that we keep the defaults when we add our custom middleware.

    The middleware property can be defined as a function which gets called with the redux-toolkit getDefaultMiddleware function. This allows you to set options for the default middleware, if you want to, while also adding our own. We will follow the docs example and write this:

    const store = configureStore({
      reducer: {
        movements: movementsSlice.reducer,
        auth: authSlice.reducer,
      },
      // Note: you can include options in the argument of the getDefaultMiddleware function call.
      middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(authMiddleware)
    });
    

    Don't do this, as it will remove all default middleware

    const store = configureStore({
      reducer: {
        movements: movementsSlice.reducer,
        auth: authSlice.reducer,
      },
      middleware: [authMiddleware]
    });
    

    Syncing State via Middleware

    We could potentially streamline our middleware by matching all auth actions. We do that by using the String.prototype.startsWith() method on the action.type (similar to the examples in the addMatcher docs section which use .endswith()).

    Here we find the next state by executing next(action) before we change localStorage. We set the localStorage value to the new state returned by the auth slice.

    const authMiddleware = (store) => (next) => (action) => {
      const result = next(action);
      if ( action.type?.startsWith('auth/') ) {
        const authState = store.getState().auth;
        localStorage.setItem('auth', JSON.stringify(authState))
      }
      return result;
    };
    

    Or you can use the redux-persist package, which does that for you.