Where to write to localStorage in a Redux app?

JavascriptReduxLocal StorageState

Javascript Problem Overview


I want to persist some parts of my state tree to the localStorage. What is the appropriate place to do so? Reducer or action?

Javascript Solutions


Solution 1 - Javascript

Reducer is never an appropriate place to do this because reducers should be pure and have no side effects.

I would recommend just doing it in a subscriber:

store.subscribe(() => {
  // persist your state
})

Before creating the store, read those persisted parts:

const persistedState = // ...
const store = createStore(reducer, persistedState)

If you use combineReducers() you’ll notice that reducers that haven’t received the state will “boot up” as normal using their default state argument value. This can be pretty handy.

It is advisable that you debounce your subscriber so you don’t write to localStorage too fast, or you’ll have performance problems.

Finally, you can create a middleware that encapsulates that as an alternative, but I’d start with a subscriber because it’s a simpler solution and does the job well.

Solution 2 - Javascript

To fill in the blanks of Dan Abramov's answer you could use store.subscribe() like this:

store.subscribe(()=>{
  localStorage.setItem('reduxState', JSON.stringify(store.getState()))
})

Before creating the store, check localStorage and parse any JSON under your key like this:

const persistedState = localStorage.getItem('reduxState') 
                       ? JSON.parse(localStorage.getItem('reduxState'))
                       : {}

You then pass this persistedState constant to your createStore method like this:

const store = createStore(
  reducer, 
  persistedState,
  /* any middleware... */
)

Solution 3 - Javascript

In a word: middleware.

Check out redux-persist. Or write your own.

[UPDATE 18 Dec 2016] Edited to remove mention of two similar projects now inactive or deprecated.

Solution 4 - Javascript

If anybody is having any problem with the above solutions, you can write your own to. Let me show you what I did. Ignore saga middleware things just focus on two things localStorageMiddleware and reHydrateStore method. the localStorageMiddleware pull all the redux state and puts it in local storage and rehydrateStore pull all the applicationState in local storage if present and puts it in redux store

import {createStore, applyMiddleware} from 'redux'
import createSagaMiddleware from 'redux-saga';
import decoristReducers from '../reducers/decorist_reducer'

import sagas from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

/**
 * Add all the state in local storage
 * @param getState
 * @returns {function(*): function(*=)}
 */
const localStorageMiddleware = ({getState}) => { // <--- FOCUS HERE
    return (next) => (action) => {
        const result = next(action);
        localStorage.setItem('applicationState', JSON.stringify(
            getState()
        ));
        return result;
    };
};


const reHydrateStore = () => { // <-- FOCUS HERE

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

    }
}


const store = createStore(
    decoristReducers,
    reHydrateStore(),// <-- FOCUS HERE
    applyMiddleware(
        sagaMiddleware,
        localStorageMiddleware,// <-- FOCUS HERE 
    )
)

sagaMiddleware.run(sagas);

export default store;

Solution 5 - Javascript

Building on the excellent suggestions and short code excerpts provided in other answers (and Jam Creencia's Medium article), here's a complete solution!

We need a file containing 2 functions that save/load the state to/from local storage:

// FILE: src/common/localStorage/localStorage.js

// Pass in Redux store's state to save it to the user's browser local storage
export const saveState = (state) =>
{
  try
  {
    const serializedState = JSON.stringify(state);
    localStorage.setItem('state', serializedState);
  }
  catch
  {
    // We'll just ignore write errors
  }
};



// Loads the state and returns an object that can be provided as the
// preloadedState parameter of store.js's call to configureStore
export const loadState = () =>
{
  try
  {
    const serializedState = localStorage.getItem('state');
    if (serializedState === null)
    {
      return undefined;
    }
    return JSON.parse(serializedState);
  }
  catch (error)
  {
    return undefined;
  }
};

Those functions are imported by store.js where we configure our store:

NOTE: You'll need to add one dependency: npm install lodash.throttle

// FILE: src/app/redux/store.js

import { configureStore, applyMiddleware } from '@reduxjs/toolkit'

import throttle from 'lodash.throttle';

import rootReducer from "./rootReducer";
import middleware from './middleware';

import { saveState, loadState } from 'common/localStorage/localStorage';


// By providing a preloaded state (loaded from local storage), we can persist
// the state across the user's visits to the web app.
//
// READ: https://redux.js.org/recipes/configuring-your-store
const store = configureStore({
	reducer: rootReducer,
	middleware: middleware,
	enhancer: applyMiddleware(...middleware),
	preloadedState: loadState()
})


// We'll subscribe to state changes, saving the store's state to the browser's
// local storage. We'll throttle this to prevent excessive work.
store.subscribe(
	throttle( () => saveState(store.getState()), 1000)
);


export default store;

The store is imported into index.js so it can be passed into the Provider that wraps App.js:

// FILE: src/index.js

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'

import App from './app/core/App'

import store from './app/redux/store';


// Provider makes the Redux store available to any nested components
render(
	<Provider store={store}>
		<App />
	</Provider>,
	document.getElementById('root')
)

Note that absolute imports require this change to YourProjectFolder/jsconfig.json - this tells it where to look for files if it can't find them at first. Otherwise, you'll see complaints about attempting to import something from outside of src.

{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": ["src"]
}

Solution 6 - Javascript

I cannot answer @Gardezi but an option based on his code could be:

const rootReducer = combineReducers({
    users: authReducer,
});

const localStorageMiddleware = ({ getState }) => {
    return next => action => {
        const result = next(action);
        if ([ ACTIONS.LOGIN ].includes(result.type)) {
            localStorage.setItem(appConstants.APP_STATE, JSON.stringify(getState()))
        }
        return result;
    };
};

const reHydrateStore = () => {
    const data = localStorage.getItem(appConstants.APP_STATE);
    if (data) {
        return JSON.parse(data);
    }
    return undefined;
};

return createStore(
    rootReducer,
    reHydrateStore(),
    applyMiddleware(
        thunk,
        localStorageMiddleware
    )
);

the difference is that we are just saving some actions, you could event use a debounce function to save only the last interaction of your state

Solution 7 - Javascript

I'm a bit late but I implemented a persistent state according to the examples stated here. If you want to update the state only every X seconds, this approach may help you:

  1. Define a wrapper function

     let oldTimeStamp = (Date.now()).valueOf()
     const millisecondsBetween = 5000 // Each X milliseconds
     function updateLocalStorage(newState)
     {
         if(((Date.now()).valueOf() - oldTimeStamp) > millisecondsBetween)
         {
             saveStateToLocalStorage(newState)
             oldTimeStamp = (Date.now()).valueOf()
             console.log("Updated!")
         }
     }
    
  2. Call a wrapper function in your subscriber

         store.subscribe((state) =>
         {
         updateLocalStorage(store.getState())
          });
    

In this example, the state is updated at most each 5 seconds, regardless how often an update is triggered.

Solution 8 - Javascript

If you don't need to copy all redux store to localStorage you can use the specific store arguments:

store.subscribe(()=>{
  window.localStorage.setItem('currency', store.getState().lang)
})

And set initial state argument value like:

const initialState = {
  currency: window.localStorage.getItem('lang') ?? 'en',
}

In this case, you don't need to pass const persistedState to const store = createStore()

Solution 9 - Javascript

I was looking badly for an entire example on how to persist state into a local storage using redux-toolkit-persist with no success until I came across @canProm response above to solve my issue. This is what is working for me

//package.json
 "reduxjs-toolkit-persist": "^7.0.1",
"lodash": "^4.17.21"

 //localstorage.ts
 import localStorage from 'reduxjs-toolkit-persist/es/storage';
  export const saveState = (state: any) => {
  try {
     console.log(state);
     const serializableState = JSON.stringify(state);
     localStorage.setItem('globalState', serializableState);
   } catch (err) {
    console.log('Redux was not able to persist the state into the localstorage');
 }
};


export const loadState = () => {
  try {
    const serializableState: string | any = 
 localStorage.getItem('globalState');
    return serializableState !== null || serializableState === undefined ? JSON.parse(serializableState) : undefined;
   } catch (error) {
      return undefined;
  }
};



//slices - actions
//reduxjs-toolkit-slices.ts
  import { combineReducers, createSlice, PayloadAction } from '@reduxjs/toolkit';
  import { UserModel } from '../model/usermodel';
  import { GlobalState } from './type';

  const deaultState: GlobalState = {
       counter: 0,
       isLoggedIn: false
  };

 const stateSlice = createSlice({
    name: "state",
   initialState: deaultState,
reducers: {
    isLoggedIn: (state, action: PayloadAction<boolean>) => {
        console.log('isLogged');
        console.log(state.isLoggedIn);
        console.log(action);
        state.isLoggedIn = action.payload;
        console.log(state.isLoggedIn);
    },
    setUserDetails: (state, action: PayloadAction<UserModel>) => {
        console.log('setUserDetails');
        console.log(state);
        console.log(action);
        //state.userContext.user = action.payload;
    }
  }
});

//export actions under slices
export const {
  isLoggedIn: isUserLoggedAction,
  setUserDetails: setUserDetailActions
} = stateSlice.actions;

//TODO: use the optimal way for combining reducer using const
//combine reducer from all slice
export const combineReducer = combineReducers({
   stateReducer: stateSlice.reducer
});

 //storeConfig
  //reduxjs-toolkit-store.ts
 import { configureStore } from '@reduxjs/toolkit';
 import { throttle } from 'lodash';
 import { persistReducer } from 'reduxjs-toolkit-persist';
 import autoMergeLevel2 from 'reduxjs-toolkit-persist/lib/stateReconciler/autoMergeLevel2';
 import storage from 'reduxjs-toolkit-persist/lib/storage';
  import { loadState, saveState } from './localStorage';
 import { combineReducer } from './reduxjs-toolkit-slices';

  // persist config
  const persistConfig = {
      key: 'root',
      storage: storage,
      stateReconciler: autoMergeLevel2,
  };
  const persistedReducer = persistReducer(persistConfig, combineReducer);

  // export reducers under slices
  const store = configureStore({
      reducer: persistedReducer,
     devTools: process.env.NODE_ENV !== 'production',
     preloadedState: loadState(), //call loadstate method to initiate store from localstorage
     middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
        thunk: true,
        serializableCheck: false,
    }),
 });
 // handle state update event. Whenever the state will change, this subscriber will call the saveState methode to update and persist the state into the store
store.subscribe(throttle(() => {
    saveState(store.getState());
 }, 1000));


export default store;

 //App.ts
 import { persistStore } from 'reduxjs-toolkit-persist';
 import { PersistGate } from 'reduxjs-toolkit-persist/integration/react';
 import './i18n';

 let persistor = persistStore(store);

  ReactDOM.render(
     <Provider store={store}>
       <PersistGate loading={<div>Loading .....</div>} persistor={persistor}>
         <HalocarburesRouter />
       </PersistGate>
     </Provider>,
   document.getElementById('ingenierieMdn'));

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMarco de JonghView Question on Stackoverflow
Solution 1 - JavascriptDan AbramovView Answer on Stackoverflow
Solution 2 - JavascriptAndrew SamuelsenView Answer on Stackoverflow
Solution 3 - JavascriptDavid L. WalshView Answer on Stackoverflow
Solution 4 - JavascriptGardeziView Answer on Stackoverflow
Solution 5 - JavascriptCanProgramView Answer on Stackoverflow
Solution 6 - JavascriptDouglas CainaView Answer on Stackoverflow
Solution 7 - JavascriptmovcmpretView Answer on Stackoverflow
Solution 8 - JavascriptTom LinksView Answer on Stackoverflow
Solution 9 - JavascriptonlymeView Answer on Stackoverflow