17

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.

Ania Kowalska
  • 415
  • 1
  • 4
  • 12
  • 1
    Hey, I'm coming here with an update. Actually nobody in the comments recommended me redux persist, which would totally satisfy me for this issue :D The most up-voted answer is a gem though, so I leave the question for others – Ania Kowalska May 11 '22 at 15:10

4 Answers4

41

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.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
  • Awesome! Thanks for help. It didn't entirely solve my problem, but I was digging through docs and other stack overflow threads - it was successfully add things to my local storage, but I was struggling how to 'send' it back to my state. But it was huge help, thank you! – Ania Kowalska Jul 17 '21 at 17:12
18

In the meantime I've written the logic for movements and wanted to keep all my state in the local storage. The answer of Linda Paiste was very helpful (and kudos for such a long and straightforward answer!), but I was struggling with sending my local storage back to my redux state. Here's the working solution:

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

const initialMovementsState = {
  movements: dummyItems,
};

const movementsSlice = createSlice({
  name: 'movements',
  initialState: initialMovementsState,
  reducers: {
    add(state, action) {
      state.movements = [action.payload, ...state.movements];
    },
    delete(state, action) {
      const id = action.payload;
      state.movements = state.movements.filter(mov => mov.id !== id);
    },
  },
});

//AUTHORIZATION
const initialAuthState = {
  isAuthenticated: false,
};

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

//MIDDLEWARE
const localStorageMiddleware = ({ getState }) => {
  return next => action => {
    const result = next(action);
    localStorage.setItem('applicationState', JSON.stringify(getState()));
    return result;
  };
};

const reHydrateStore = () => {
  if (localStorage.getItem('applicationState') !== null) {
    return JSON.parse(localStorage.getItem('applicationState')); // re-hydrate the store
  }
};

//STORE CONFIGURATION
const store = configureStore({
  reducer: {
    movements: movementsSlice.reducer,
    auth: authSlice.reducer,
  },
  preloadedState: reHydrateStore(),
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware().concat(localStorageMiddleware),
});

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

export default store;

Ania Kowalska
  • 415
  • 1
  • 4
  • 12
-2
import {configureStore} from '@reduxjs/toolkit';

import userReducer from '../userSlices';

const store =  configureStore({

    reducer:{
        user:userReducer,
    },
    preloadedState:loadFromLocalStorage()
});

function saveToLocalStorage(state){

    try{
      const serialState = JSON.stringify(state)
      localStorage.setItem("reduxStore",serialState)
    }catch(e){
      console.warn(e);
    }
  }
  
  function loadFromLocalStorage(){

    try{
      const serialisedState = localStorage.getItem("reduxStore");
      if(serialisedState === null) return undefined;
      return JSON.parse(serialisedState);
    }catch(e){
      console.warn(e);
      return undefined;
    }
  }

  store.subscribe(()=>saveToLocalStorage(store.getState()));
    
export default store;
cigien
  • 57,834
  • 11
  • 73
  • 112
  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 17 '23 at 13:30
-3

You must dispatch the login or logout action to actually changes your state in the redux store!

  • Just simple use `dispatch(login())` or `dispatch(logout())`. And you should change `isAuthenticated` in `localStorage` after you dispatch your action. – Trung-Tin Pham Jul 17 '21 at 14:08
  • I tried like this (like I mentioned in my question) and it doesn't work. It always sets it to true after refreshing. – Ania Kowalska Jul 17 '21 at 14:16