187

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

Damjan Pavlica
  • 21,431
  • 6
  • 55
  • 65
Marco de Jongh
  • 4,604
  • 3
  • 15
  • 29

9 Answers9

249

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.

Dan Abramov
  • 241,321
  • 75
  • 389
  • 492
  • 1
    what If I would like to subscribe to just part of the state? is that possible? I went ahead with something a little bit different: 1. save persisted state outside of redux. 2. load persisted state inside react constructor(or with componentWillMount) with redux action. Is 2 is totally fine instead of loading the persisted data on store directly? (which I'm trying to keep separately for SSR). By the way thanks for Redux! It's awesome, I love it, after getting lost with my big project code now it's all back to simple and predictable :) – Mark Uretsky Sep 27 '16 at 08:19
  • within store.subscribe callback, you have full access to the current store data , so you can persist any part you are interested in – Bo Chen Nov 21 '16 at 06:45
  • 38
    Dan Abramov has also made a whole video on it: https://egghead.io/lessons/javascript-redux-persisting-the-state-to-the-local-storage – NateW Feb 04 '17 at 05:07
  • 2
    Are there legitimate performance concerns with serializing the state on every store update? Does the browser serialize on a separate thread maybe? – Stephen Paul Aug 25 '17 at 07:13
  • Edit: Actually, I watched your video, and I suppose throttling the subscription does help – Stephen Paul Aug 25 '17 at 07:19
  • 9
    This looks potentially wasteful. OP did say that only a part of the state needs to be persisted. Let's say you have a store with 100 different keys. You only want to persist 3 of them that are infrequently changed. Now you are parsing and updating local store on every little change to any of your 100 keys, all the while none of the 3 keys you're interested in persisting have ever been even changed. Solution by @Gardezi below is a better approach as you can add event listeners into your middleware so that you *only* update when you actually need to e.g.: https://codepen.io/retrcult/pen/qNRzKN – Anna T Jul 26 '18 at 18:16
  • If you are worried about performance, use [redux-persist](https://github.com/rt2zz/redux-persist). It [serializes only keys that have changed](https://github.com/rt2zz/redux-persist/issues/449#issuecomment-330107909). – Radek Matěj Aug 22 '20 at 12:52
167

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... */
)
Volkan Güven
  • 654
  • 2
  • 11
  • 25
Andrew Samuelsen
  • 4,885
  • 8
  • 43
  • 65
  • 6
    Simple and effective, without extra dependencies. – AxeEffect Oct 14 '16 at 00:29
  • 1
    creating a middleware may look a bit better, but I agree that it's effective and is sufficient to most cases – Bo Chen Nov 21 '16 at 06:45
  • 1
    Is there a nice way to do this when using combineReducers, and you only want to persist one store? – brendangibson Jan 04 '17 at 21:08
  • @brendangibson you would put the data you want into a particular key of an object to be passed as "persistedState" above. For example if you only wanted to persist stuff to your "meta" reducer you would pass {meta:metaStateIJustGotFromLocalStorage}. This answer is super helpful: http://stackoverflow.com/a/33791942/607569 – Andrew Samuelsen Jan 04 '17 at 22:55
  • 1
    If nothing is in localStorage, should `persistedState` return `initialState` instead of an empty object? Otherwise I think `createStore` will initialize with that empty object. – Alex Feb 21 '17 at 17:05
  • 1
    @Alex You're right, unless your initialState is empty. – Link14 Mar 03 '17 at 03:24
  • Ok cool. This was only an issue for me when I had a single parent reducer with an initial state. After I broke up the reducers into multiple files and used `combineReducers`, it could be initialized with `{}` again since the initial state was now defined within each reducer. – Alex Mar 03 '17 at 15:32
  • @AndrewSamuelsen I'm getting this error. Can you tell me what is it that I'm missing `TypeError: Cannot read property 'subscribe' of undefined` – Gardezi Feb 11 '18 at 17:06
  • @Gardezi seems you're either not setting a store or doing it incorrectly. Do you have a line like this (`const store = createStore(reducer, persistedState)`) above accessing `.subscribe`? – Andrew Samuelsen Feb 14 '18 at 20:30
  • @AndrewSamuelsen i used a little bit of your idea and constructed a solution. Please take a look at it I have posted it down and if I can make it better I would love to hear about it :) – Gardezi Feb 15 '18 at 06:09
  • I needed to change `persistedState` from `{}` to `undefined` in ternary, to work fine. – Damjan Pavlica Jul 19 '18 at 16:48
  • As per my experience JSON.stringify and JSON.parse have side effects on "Date" objects https://stackoverflow.com/q/11491938/1102016 – Nat Geo Jan 03 '19 at 05:53
50

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.

David L. Walsh
  • 20,838
  • 7
  • 55
  • 45
13

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;
Gardezi
  • 2,064
  • 2
  • 27
  • 52
  • 2
    Hi, wouldn't this result in a lot of writes to `localStorage` even when nothing in the store has changed? How you compensate for unnecessary writes – user566245 Sep 30 '18 at 17:53
  • Well, It works, anyways it this good idea to have? Question : What happend in case of we have more data menas multiple reducers with large data? – Kunvar Singh Mar 04 '20 at 11:12
5

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

Douglas Caina
  • 145
  • 1
  • 5
4

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"]
}
CanProgram
  • 323
  • 1
  • 11
1

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.

movcmpret
  • 125
  • 6
  • 2
    You could wrap ```(state) => { updateLocalStorage(store.getState()) }``` in ```lodash.throttle``` like this: ```store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }``` and remove time checking logic inside. – MR.QUESTION Feb 20 '20 at 13:55
0

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()

0

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'));
onlyme
  • 2,606
  • 2
  • 17
  • 13