The Definitive Guide to Redux Persist | by Mark Newton


Redux Persist takes your Redux state object and saves it to persisted storage. Then on app launch it retrieves this persisted state and saves it back to redux.

Notes:

  • This guide is for v5 of redux-persist, which was released in October 2017.
  • Parts of this guide were merged into the official docs via this pull request, which I submitted. However, this guide is still your best source for gaining an understanding of the library.

npm install --save redux-persist – OR – yarn add redux-persist

When creating your redux store, pass your createStore function a persistReducer that wraps your app’s root reducer. Once your store is created, pass it to the persistStore function, which ensures your redux state is saved to persisted storage whenever it changes.

// src/store/index.js

import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';

import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import rootReducer from './reducers'; // the value from combineReducers

const persistConfig = {
key: 'root',
storage: storage,
stateReconciler: autoMergeLevel2 // see "Merge Process" section for details.
};

const pReducer = persistReducer(persistConfig, rootReducer);

export const store = createStore(pReducer);
export const persistor = persistStore(store);

If you are using React, wrap your root component with PersistGate. This delays the rendering of your app’s UI until your persisted state has been retrieved and saved to redux.

import React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/lib/integration/react';

// import the two exports from the last code snippet.
import { persistor, store } from './store';
// import your necessary custom components.
import { RootComponent, LoadingView } from './components';

const App = () => {
return (
<Provider store={store}>
// the loading and persistor props are both required!
<PersistGate loading={<LoadingView />} persistor={persistor}>
<RootComponent />
</PersistGate>
</Provider>
);
};

export default App;

If you don’t want to persist a part of your state you could put it in the blacklist. The blacklist is added into the config object that we used when setting up our PersistReducer.

const persistConfig = {
key: 'root',
storage: storage,
blacklist: ['navigation']
};

const pReducer = persistReducer(persistConfig, rootReducer);

export const store = createStore(pReducer);
export const persistor = persistStore(store);

The blacklist takes an array of strings. Each string must match a part of state that is managed by the reducer you passed to persistReducer. For the example above, if rootReducer was created via the combineReducers function we would expect ‘navigation’ to appear there, like so:

combineReducers({ 
auth: AuthReducer,
navigation: NavReducer,
notes: NotesReducer
});

The whitelist is set up in the same way as the blacklist except that it defines the parts of state that you do want to persist.

const persistConfig = {
key: 'root',
storage: storage,
whitelist: ['auth', 'notes']
};

What if you wanted to blacklist a nested property though? For example, let’s say your state object has an auth key and that you want to persist auth.currentUser but NOT auth.isLoggingIn.

To do this, wrap your AuthReducer with a PersistReducer, and then blacklist the isLoggingIn key. This allows you to co-locate your persistence rules with the reducer it pertains to.

// AuthReducer.js
import storage from 'redux-persist/lib/storage';
import { persistReducer } from 'redux-persist';

const INITIAL_STATE = {
currentUser: null,
isLoggingIn: false
};

const AuthReducer = (state = INITIAL_STATE, action) => {
// reducer implementation
};

const persistConfig = {
key: 'auth',
storage: storage,
blacklist: ['isLoggingIn']
};

export default persistReducer(persistConfig, AuthReducer);

If you prefer to have all your persistence rules in one place, instead of co-located with their associated reducer, consider putting it all with your combineReducers function:

// src/reducers/index.js

import { combineReducers } from 'redux';
import storage from 'redux-persist/lib/storage';
import { persistReducer } from 'redux-persist';

import { authReducer, navReducer, notesReducer } from './reducers'

const rootPersistConfig = {
key: 'root',
storage: storage,
blacklist: ['navigation']
};

const authPersistConfig = {
key: 'auth',
storage: storage,
blacklist: ['isLoggingIn']
};

const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer),
navigation: navReducer,
notes: notesReducer
});

export default persistReducer(rootPersistConfig, rootReducer);

When your app launches, redux sets an initial state. Shortly after this, Redux Persist retrieves your persisted state from storage. Your persisted state then overrides any initial state.

The merge process is meant to “just work” automatically for you. However, you can also take manual control of the process. For example, in older versions of Redux Persist it was common to manage the rehydration process by catching the REHYDRATE action in your reducers and then saving the action’s payload to your redux state.

import { REHYDRATE } from 'redux-persist';

const INITIAL_STATE = {
currentUser: null,
isLoggingIn: false
};

const AuthReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {

case REHYDRATE:
return {
...state,
currentUser: action.payload.currentUser
};

// ...handle other cases

The REHYDRATE action is dispatched by Redux Persist immediately after your persisted state is obtained from storage. If you return a new state object from the REHYDRATE action, this will be your finalized state. As mentioned though, you don’t need to do this anymore unless you need to customize the way your state is rehydrated.

There is a gotcha when it comes to the merge process, and it has to do with how deeply the merge process looks inside your state for changes. We mentioned that the merge process overrides your initial state with whatever was persisted. Here is how that works by default.

Let’s say our initial state looks like this, and that we are persisting the entire thing.

// initial state
{
auth: {
currentUser: null,
isLoggingIn: false
},
notes: []
}

Our app launches, and here is our persisted state.

// persisted state
{
auth: {
currentUser: { firstName: 'Mark', lastName: 'Newton' },
isLoggingIn: false
},
notes: [noteA, noteB, noteC]
}

By default, the merge process simply replaces each top-level piece of state. In code, this looks similar to the following:

const finalState = { ...initialState };finalState['auth'] = persistedState['auth']
finalState['notes'] = persistedState['notes']

This usually works fine, but what if you released a new version of your app that sets your initial auth state like this.

const INITIAL_STATE = {
currentUser: null,
isLoggingIn: false,
error: ''
};

You’d obviously want your final state object to include this new error key. But your persisted state object doesn’t yet have this error key, and its going to completely replace your initial state during the rehydration process. Bye bye error key.

The fix for this is to tell your PersistReducer to merge two-levels deep. In the Quickstart section, you may have noticed a mysterious stateReconciler setting for our root PersistReducer.

import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';const persistConfig = {
key: 'root',
storage: storage,
stateReconciler: autoMergeLevel2
};

autoMergeLevel2 is how you merge two-levels deep. For the auth state, this means that the merge process will first make a copy of the initial auth state, and then only override the keys within this auth object that were persisted. Since ‘error’ wouldn’t have been persisted yet, it would be left alone.

In summary, it’s important to know that PersistReducers default to autoMergeLevel1, which means they replace top-level state with whatever was persisted. If you don’t have a separate PersistReducer managing the persisted state for these top-level keys, you’ll probably want to use autoMergeLevel2.

Interesting tidbit: the author of Redux Persist realized that choosing between autoMergeLevel1 and autoMergeLevel2 can be confusing. So he created a function called persistCombineReducers in an attempt to simplify things. This function’s implementation is two lines of code, and simply returns a PersistReducer defaulted to autoMergeLevel2. My personal preference is to set the merge level myself, and not use this function. But it’s up to you.

Transforms allow you to customize the state object that gets persisted and rehydrated.

​When the state object gets persisted, it first gets serialized with JSON.stringify(). If parts of your state object are not mappable to JSON objects, the serialization process may transform these parts of your state in unexpected ways. For example, the javascript Set type does not exist in JSON. When you try to serialize a Set via JSON.stringify()​, it gets converted to an empty object. Probably not what you want.

Below is a Transform that successfully persists a Set​ property, which simply converts it to an array and back. In this way, the Set gets converted to an Array, which is a recognized data structure in JSON. When pulled out of the persisted store, the array gets converted back to a Set before being saved to the redux store.

import { createTransform } from 'redux-persist';

const SetTransform = createTransform(

// transform state on its way to being serialized and persisted.
(inboundState, key) => {
// convert mySet to an Array.
return { ...inboundState, mySet: [...inboundState.mySet] };
},

// transform state being rehydrated
(outboundState, key) => {
// convert mySet back to a Set.
return { ...outboundState, mySet: new Set(outboundState.mySet) };
},

// define which reducers this transform gets called for.
{ whitelist: ['someReducer'] }
);

export default SetTransform;

The createTransform function takes three parameters.

  • A function that gets called right before state is persisted.
  • A function that gets called right before state is rehydrated.
  • A config object.

Lastly, transforms need to be added to a PersistReducer’s config object.

import storage from 'redux-persist/lib/storage';
import { SetTransform } from './Transforms';
const persistConfig = {
key: 'root',
storage: storage,
transforms: [SetTransform]
};
// ...remaining implementation

Have questions? Leave me a comment!



Source link

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top