101

The first principal of Redux documentation is:

The state of your whole application is stored in an object tree within a single store.

And I actually thought that I understand all of principals well. But I'm now confused what does application means.

If application means just one of little complicated part in website and works in just one page, I understand. But what if application means whole website? Should I use LocalStorage or cookie or something for keep the state tree? but what if browser doesn't support LocalStorage?

I want to know how developers keep their state tree! :)

incleaf
  • 1,505
  • 3
  • 13
  • 11
  • 2
    That's kind of a broad question. You can do any of the things you mentioned. Do you have code you'd like to share to show us what you tried and didn't work? You can implement your whole website to be a single entity, or you can have multiple. You can use localStorage to persist data, or a real DB, or neither. Application means living, active instance. In most cases this is just one, your root. But, again, there are many ways to implement applications. – ZekeDroid May 12 '16 at 19:22

3 Answers3

95

If you would like to persist your redux state across a browser refresh, it's best to do this using redux middleware. Check out the redux-persist and redux-storage middleware. They both try to accomplish the same task of storing your redux state so that it may be saved and loaded at will.

--

Edit

It's been some time since I've revisited this question, but seeing that the other (albeit more upvoted answer) encourages rolling your own solution, I figured I'd answer this again.

As of this edit, both libraries have been updated within the last six months. My team has been using redux-persist in production for a few years now and have had no issues.

While it might seem like a simple problem, you'll quickly find that rolling your own solution will not only cause a maintenance burden, but result in bugs and performance issues. The first examples that come to mind are:

  1. JSON.stringify and JSON.parse can not only hurt performance when not needed but throw errors that when unhandled in a critical piece of code like your redux store can crash your application.
  2. (Partially mentioned in the answer below): Figuring out when and how to save and restore your app state is not a simple problem. Do it too often and you'll hurt performance. Not enough, or if the wrong parts of state are persisted, you may find yourself with more bugs. The libraries mentioned above are battle-tested in their approach and provide some pretty fool-proof ways of customizing their behavior.
  3. Part of the beauty of redux (especially in the React ecosystem) is its ability to be placed in multiple environments. As of this edit, redux-persist has 15 different storage implementations, including the awesome localForage library for web, as well as support for React Native, Electron, and Node.

To sum it up, for 3kB minified + gzipped (at the time of this edit) this is not a problem I would ask my team to solve itself.

michaelgmcd
  • 1,959
  • 16
  • 24
  • 5
    I can recommend redux-persist (have not tried redux-storage yet) but it works pretty good for me with just very little configuration and setup. – larrydahooster May 13 '16 at 08:05
  • As of this date both the libraries seam to be dead and not maintained with last commits as far back as 2 yrs ago. – AnBisw Nov 11 '18 at 19:38
  • 1
    looks like redux-persist is back a bit, with a new publish 22 days ago at time of my writing – Zeragamba Jan 01 '19 at 01:44
  • The new location of redux-storage is https://github.com/react-stack/redux-storage – Michael Freidgeim Aug 23 '19 at 09:36
  • 2
    **NOTE THIS ABOUT THIS ANSWER:** The reality is that software & libraries have generally adopted community(support) based approach that **even some very important modules of a programming language are supported by third parties/libraries.** Generally, developer has to keep an eye on every tool used in his stack to know whether it's getting deprecated/updated or not. Two choices; **1.** Implement your own & keep developing forever ensuring performance & cross platform standards. **2.** Use **battle-tested** solution & only check updates/recommendations as @MiFreidgeimSO-stopbeingevil says – Geek Guy Nov 23 '19 at 09:13
81

Edit 25-Aug-2019

As stated in one of the comments. The original redux-storage package has been moved to react-stack. This approach still focuses on implementing your own state management solution.


Original Answer

While the provided answer was valid at some point it is important to notice that the original redux-storage package has been deprecated and it's no longer being maintained...

The original author of the package redux-storage has decided to deprecate the project and no longer maintained.

Now, if you don't want to have dependencies on other packages to avoid problems like these in the future it is very easy to roll your own solution.

All you need to do is:

1- Create a function that returns the state from localStorage and then pass the state to the createStore's redux function in the second parameter in order to hydrate the store

 const store = createStore(appReducers, state);

2- Listen for state changes and everytime the state changes, save the state to localStorage

store.subscribe(() => {
    //this is just a function that saves state to localStorage
    saveState(store.getState());
}); 

And that's it...I actually use something similar in production, but instead of using functions, I wrote a very simple class as below...

class StateLoader {

    loadState() {
        try {
            let serializedState = localStorage.getItem("http://contoso.com:state");

            if (serializedState === null) {
                return this.initializeState();
            }

            return JSON.parse(serializedState);
        }
        catch (err) {
            return this.initializeState();
        }
    }

    saveState(state) {
        try {
            let serializedState = JSON.stringify(state);
            localStorage.setItem("http://contoso.com:state", serializedState);

        }
        catch (err) {
        }
    }

    initializeState() {
        return {
              //state object
            }
        };
    }
}

and then when bootstrapping your app...

import StateLoader from "./state.loader"

const stateLoader = new StateLoader();

let store = createStore(appReducers, stateLoader.loadState());

store.subscribe(() => {
    stateLoader.saveState(store.getState());
});

Hope it helps somebody

Performance Note

If state changes are very frequent in your application, saving to local storage too often might hurt your application's performance, especially if the state object graph to serialize/deserialize is large. For these cases, you might want to debounce or throttle the function that saves state to localStorage using RxJs, lodash or something similar.

Community
  • 1
  • 1
Leo
  • 13,933
  • 2
  • 34
  • 53
  • 11
    Instead of using middleware, I prefer this approach. Thanks for the tips regards with performance concern. – Joe zhou Jan 26 '18 at 19:05
  • 1
    Definitely the preferred answer. However, when I refresh the page and it loads the state from localstorage when it creates the store, I get several warnings which include the text "Unexpected properties [container names] found in previous state received by the reducer. Expected to find one of the known reducer property names instead: "global", "language". Unexpected properties will be ignored. It still works, and is basically complaining that at the point of creating the store it doesn't know about all those other containers. Is there a way around this warning? – Zief Nov 30 '18 at 12:01
  • 1
    @Zief hard to say. The message "seems" quite clear, the reducers are expecting properties that are not specified. It might be something related to providing default values to the serialized state? – Leo Mar 13 '19 at 14:14
  • Very straightforward solution. Thank you. – Ishara Madawa Mar 21 '19 at 11:12
  • @Leo I followed the same but localStorage is now not getting cleared even after `localStorage.clear()`. – Adil May 27 '19 at 02:42
  • 1
    @Joezhou would love to hear why you prefer this approach. Personally, this seems like the exact thing middleware was intended for. – michaelgmcd Jul 04 '19 at 06:23
  • The redux-storage is not deprecated, but just moved. The new location of redux-storage is https://github.com/react-stack/redux-storage – Michael Freidgeim Aug 25 '19 at 02:29
  • [This link is helpful for anyone using this anwer](https://stackoverflow.com/questions/37195590/how-can-i-persist-redux-state-tree-on-refresh#comment104261429_37197370) – Geek Guy Nov 23 '19 at 09:15
  • if we have to use `localStorage` then why do we need redux? Just because the state is available in multiple environments? – Kishor Pawar Jan 23 '20 at 12:07
3

This is based on Leo's answer (which should be the accepted answer since it achieves the question's purpose without using any 3rd party libs).

I've created a Singleton class that creates a Redux Store, persists it using local storage and allows simple access to its store through a getter.

To use it, just put the following Redux-Provider element around your main class:

// ... Your other imports
import PersistedStore from "./PersistedStore";

ReactDOM.render(
  <Provider store={PersistedStore.getDefaultStore().store}>
    <MainClass />
  </Provider>,
  document.getElementById('root')
);

and add the following class to your project:

import {
  createStore
} from "redux";

import rootReducer from './RootReducer'

const LOCAL_STORAGE_NAME = "localData";

class PersistedStore {

  // Singleton property
  static DefaultStore = null;

  // Accessor to the default instance of this class
  static getDefaultStore() {
    if (PersistedStore.DefaultStore === null) {
      PersistedStore.DefaultStore = new PersistedStore();
    }

    return PersistedStore.DefaultStore;
  }

  // Redux store
  _store = null;

  // When class instance is used, initialize the store
  constructor() {
    this.initStore()
  }

  // Initialization of Redux Store
  initStore() {
    this._store = createStore(rootReducer, PersistedStore.loadState());
    this._store.subscribe(() => {
      PersistedStore.saveState(this._store.getState());
    });
  }

  // Getter to access the Redux store
  get store() {
    return this._store;
  }

  // Loading persisted state from localStorage, no need to access
  // this method from the outside
  static loadState() {
    try {
      let serializedState = localStorage.getItem(LOCAL_STORAGE_NAME);

      if (serializedState === null) {
        return PersistedStore.initialState();
      }

      return JSON.parse(serializedState);
    } catch (err) {
      return PersistedStore.initialState();
    }
  }

  // Saving persisted state to localStorage every time something
  // changes in the Redux Store (This happens because of the subscribe() 
  // in the initStore-method). No need to access this method from the outside
  static saveState(state) {
    try {
      let serializedState = JSON.stringify(state);
      localStorage.setItem(LOCAL_STORAGE_NAME, serializedState);
    } catch (err) {}
  }

  // Return whatever you want your initial state to be
  static initialState() {
    return {};
  }
}

export default PersistedStore;
thgc
  • 1,935
  • 1
  • 16
  • 23