Quick Links

Redux simplifies state management in complex applications. As the Redux store contains your app's entire state, persisting it lets you save and restore the user's session.

Creating Your Store

We'll assume you're familiar with Redux fundamentals.

For this tutorial, we'll use a barebones store with a naive reducer.

import {createStore} from "redux";

const state = {authenticated: false};

const reducer = (state, action) => ({...state, ...action});

const store = createStore(reducer, state);

This trivial example sets the stage for a Redux store that can track whether we're logged in. Most users will expect to remain signed in when they return to your app. At the moment, the state is created anew each time the app loads, so users will only remain authenticated within the current session.

Adding Redux Persist

Redux Persist is a popular library which lets you add persistence to the store. The library will automatically save the store each time the state updates. You don't need to write any persistence code in your actions or reducers.

Begin by installing Redux Persist using npm:

npm install redux-persist

You now need to connect the library to your store. Wrap your root reducer using Redux Persist's persistReducer function. This lets Redux Persist inspect the actions you dispatch to your store. You'll also need to call persistStore() to start off the persistence.

import {createStore} from "redux";

import {persistStore, persistReducer} from "redux-persist";

import storage from "redux-persist/lib/storage";

const state = {authenticated: false};

const reducer = (state, action) => ({...state, ...action});

const persistConfig = {

key: "root",

storage

};

const persistedReducer = persistReducer(persistConfig, reducer);

const store = createStore(persistedReducer, state);

const persistor = persistStore(store);

This configuration is now ready to use. With only a few lines of code, we've ensured any Redux state changes will be persisted automatically. Users will stop getting signed out each time they reload your app.

Our reducer is enhanced by persistReducer() to include persistence support. This newly wrapped reducer is then passed to createStore() instead of the original. Finally, persistStore() is called, passing in the store instance, to enable persistence.

Configuring Redux Persist

The persistReducer() function accepts a configuration object as its first parameter. You must specify the key and storage properties.

key sets the name of the top-level property in the persisted object. Your store's state will be saved as the value of this property.

storage defines the storage engine to use. Redux Persist supports multiple different storage backends depending on the environment. For web use, the localStorage and sessionStorage APIs are both supported as well as basic cookies. Options are also available for React Native, Node.js, Electron and several other platforms.

You define the storage engine to use by importing it from its package. Its main API-implementing object must then be passed as the storage option to Redux Persist.

You can implement your own storage engine to use a custom persistence mechanism. Create an object with setItem(), getItem() and removeItem() methods. Redux Persist is asynchronous so each method must return a Promise that resolves when the operation is complete.

The Persistor Object

The persistor object returned from persistStore() calls has a few utility methods to let you manage persistence.

You can pause and resume persistence using the pause() and resume() methods respectively. You can force an immediate write to the storage engine with flush(). This can be helpful if you need to guarantee your state is persisted after a particular operation.

You can purge all persisted data from the storage engine using .purge(). In most cases, this should be avoided - you should use a Redux action to clear your store, which would then automatically propagate to the persisted data.

State Reconciliation

Redux Persist supports three different ways of hydrating your store from persisted state. Hydration occurs automatically when you call persistStore() and existing data is found in the storage engine. Redux Persist needs to inject that initial data into your store.

The default strategy is to merge objects up to one level deep. Any nested objects won't be merged - the incoming change will overwrite anything already in your state.

  • Persisted state: {"demo": {"foo": "bar"}}
  • State in store: {"demo": {"example": test"}}
  • Resulting hydrated store: {"demo": {"foo": "bar"}}

You may optionally switch to merging objects up to levels deep. Import the new state reconciler and add it to your store's configuration:

// usual imports omitted

import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";

const persistConfig = {

key: "root",

storage,

stateReconciler: autoMergeLevel2

};

// store configuration omitted

Here's what the result of autoMergeLevel2 would be when hydrating the example above:

  • Persisted state: {"demo": {"foo": "bar"}}
  • State in store: {"demo": {"example": test"}}
  • Resulting hydrated store: {"demo": {"foo": "bar", "example": "test"}}

The values of the demo properties from the two sources are combined in the hydration.

Use the hardSet reconciler if you want to disable merging altogether. This will replace the store's state with the contents of the storage engine. This is often undesirable as it makes migrations more complicated - if you add a new initial property to your state, it will be unset for existing users as soon as their session hydrates.

Migrating Your State

On the subject of migrations, Redux Persist has built-in support for upgrading persisted state to a new version. Sometimes you might replace properties with newer alternatives. You need to be sure that existing users won't have to reset your app to keep using it.

Migrations are configured using the migrate configuration key. The simplest approach is to pass a function which takes the state as a parameter and returns the migrated state. You also need to set the version configuration key so that Redux Persist can identify when migrations are needed. Each time the version changes, your migration function will be called.

const persistConfig = {

key: "root",

storage,

version: 1,

migrate: (state) => ({...state, oldProp: undefined, newProp: "foobar"});

};

As an alternative to the function approach, you may pass an object that enables individual migration functions to be created for each version step. This must be passed to the createMigrate() function before being handed to Redux Persist's configuration.

// other imports omitted

import {createMigrate} from "redux-persist";

const migrations = {

1: state => ({...state, extraProp: true}),

2: state => ({...state, extraProp: undefined, extraPropNew: true})

};

const persistConfig = {

key: "root",

storage,

version: 2,

migrate: createMigrate(migrations)

}

In this example, we're initialising the store as version 2. If the state already existed on the user's device as version 0, both migrations would be run. If the user was currently on version 1, only the last migration would run.

Applying Transformations

A final point to mention is that Redux Persist supports the use of "transformation" functions. These are added to your configuration and allow you to manipulate the data that is saved or restored.

The library's documentation lists several popular transformations you can use. These let you automatically compress, encrypt or expire your persisted state, without having to implement any application-level logic yourself.

Transformations are specified as an array in your configuration object. They are executed in the order given.

const persistStore = {

key: "root",

storage,

transforms: [MyTransformer]

};

To write your own transformer, use the createTransform() function. This is passed two functions and a configuration object:

import {createTransform} from "redux-persist";

const MyTransformer = createTransform(

(inboundState, key) => ({...inboundState, b64: btoa(inboundState.b64)}),

(outboundState, key) => ({...outboundState, b64: atob(outboundState.b64)}),

{}

);

In this example, we store the b64 property of our state as its Base64-encoded value. When the data is persisted to storage (outboundState), the value gets encoded. It is decoded when the persisted state is being hydrated (inboundState).

The configuration object can be used to define a whitelist and blacklist of reducer names. The transformer would then only be used with reducers which match those constraints.

Conclusion

Redux Persist is a powerful library with a simple interface. You can setup automatic persistence of your Redux store in only a few lines of code. Users and developers alike will be thankful for its convenience.