Mastering redux-persist: An Integration Guide for React Native Developers (with code examples)

Nik L. - Nov 20 '23 - - Dev Community

Thought of delving more in React, after these articles:


Introduction to Redux Persist:

This article introduces redux-persist, a tool designed to streamline the process of preserving state from a Redux store to local device storage. Particularly, it focuses on its application with AsyncStorage in React Native. Redux-persist not only facilitates state persistence but also restores the Redux store during subsequent app launches, a process commonly known as rehydration. The featured utilities significantly reduce the effort required to persist on-device data, encompassing items like authentication tokens and account settings.

Automated Process with Redux-persist:

The implementation of redux-persist automates the state persistence process, demanding only a minimal amount of boilerplate for initialization. This article adopts AsyncStorage as the chosen storage mechanism. The coverage spans the entire setup, providing comprehensive guidance on how to organize reducers through nesting for improved compatibility with redux-persist.

Efficiency with Well-Designed Reducers:

One crucial aspect often overlooked by other online resources is the effective collaboration of redux-persist with well-designed reducers. Crafting granular reducers becomes essential as the app grows in complexity, ensuring the efficient management of the Redux store. A dedicated section in this article emphasizes the importance of this practice, aligning with a successful redux-persist implementation.

Platform Independence:

Notably, redux-persist stands out for its platform-agnostic nature, making no assumptions about the development platform. It accommodates various data-persisting methods, such as localStorage for browser-based applications. Currently in version 6, the package boasts over 400,000 weekly downloads and has become a standard component in numerous Redux projects, simplifying state management.

Stability and Future Updates:

Given the overall stability of Redux, this article notes that there have been no recent updates to the redux-persist package. Future updates are deemed unlikely unless they involve breaking changes to Redux.

Ensuring Persistent App State with Redux Persist:

Syncing State Between Sessions:
Redux-persist plays a crucial role in syncing and maintaining app state across different sessions. It guarantees that the state remains saved between app sessions, initializing the Redux store with any previously saved state during app launch. This synchronization ensures that the persisted app state stays in harmony with the current app state. Without this kind of state persistence, the app would reset to its default Redux store settings after each app closure.

Authentication Token Scenario:
Consider the scenario of an authentication token that keeps a user logged into the app. The goal is for this token to persist for as long as it remains valid. With redux-persist, every time the user opens the app, the process of fetching a previously saved store, known as rehydration, comes into play. This article delves deeper into the intricacies of this process.

Understanding the Redux Store:
Throughout this article, the term "store" refers to the Redux store, a JSON object representing the app state managed through reducers in Redux. For readers unfamiliar with Redux, a brief introductory article, "Redux for React: A Simple Introduction," is recommended.

Managing Processes Separately:
While it's acceptable to manage processes like fetching tokens separately from Redux by manually calling AsyncStorage, especially as the store updates or key state changes occur, this approach may pose challenges as the app grows in complexity. The subsequent sections will explore scenarios that Redux Persist addresses and discuss effective reducer structuring before diving into the integration of redux-persist into a React Native project.

Solving the Redux Persist Dilemma:

Key Benefits of Redux Persist:
Redux Persist offers two significant advantages:

  1. Boilerplate Code Reduction:

    • Minimizes the need for extensive boilerplate code, ensuring streamlined management of your app state through Redux.
  2. Elimination of Manual AsyncStorage Calls:

    • Removes the necessity for manual AsyncStorage calls to persist app state. Traditionally, developers would write separate state management calls for Redux and AsyncStorage, but redux-persist simplifies this process.

Illustrative Scenario Without Redux Persist:
To grasp the implications of not using redux-persist, consider an article that discusses loading more list items via scrolling, titled "React Native Lists: Load More by Scrolling."

In this approach, both Redux and AsyncStorage are concurrently employed to update app state as the list is scrolled. While functional, this method introduces challenges:

Challenges in Managing Dual Data Sources:

  1. Ambiguity in Identifying the True Data Source:

    • Determining the true source of data becomes unclear; the persisted state takes precedence at app launch, while the Redux store becomes the primary source during active user interaction.
  2. Code Messiness with Simultaneous Redux and AsyncStorage Calls:

    • Making simultaneous calls to both Redux and AsyncStorage complicates the code, making it harder to read and maintain.
  3. Difficulty in Synchronizing Key-Value Storage with Redux State:

    • Matching key-value storage with Redux state storage becomes increasingly complex. Copying the entire store to AsyncStorage has drawbacks, such as potential inconsistencies during app updates.
  4. Challenges in Major Refactors:

    • Major refactors may be impractical for individuals or small teams, requiring time-consuming utilities to refactor structured data. Alternatively, wiping persisted data and prompting the user to sign in again may be a solution.

Optimizing Data Management with Redux Persist:
To address these challenges, it is crucial to eliminate the AsyncStorage component from the data flow diagram and rely solely on a Redux store as the primary data source. Redux-persist plays a pivotal role in seamlessly keeping data in sync and rehydrating it during subsequent app launches.

Leveraging Reducers for Efficient Store Structure:
Before delving into the implementation of redux-persist, let's explore how to leverage reducers to structure your Redux store efficiently. As we'll discuss later, redux-persist allows the persistence or omission of specific subsets of the Redux store. Therefore, structuring reducers with the intention of inclusion or exclusion in persisted state becomes a crucial consideration.

Structuring and Nesting Reducers for Redux-Persist Integration:

Modularizing Reducers for Enhanced Efficiency:
Efficiently structuring reducers in a modular fashion is vital to fully leverage the capabilities offered by redux-persist.

1. Basic Multi-Reducer Setup:
Up until now, a common approach involves combining reducers from a single index file with the following structure:

// simple multi-reducer setup
reducers/
   account.js
   auth.js
   index.js
   plans.js
   sessions.js
Enter fullscreen mode Exit fullscreen mode

The index.js file then combines these reducer files using combineReducers:

// reducers/index.js
import account from './account'
import auth from './auth'
...

export default combineReducers({
   account,
   auth,
   ...
});
Enter fullscreen mode Exit fullscreen mode

This creates a 1-level deep reducer tree, suitable for simpler apps. However, there are ways to enhance this setup for better modularity.

2. Calling Reducers from Other Reducers:
Reducers, by default, adopt the key of their name. For example, the auth reducer manages state updates to the auth namespace and is accessed at store.auth from the store state. This default key can be changed by providing a key inside combineReducers, allowing for a more idiomatic naming convention:

// providing a reducer namespace manually
import authReducer from './auth'
...

export default combineReducers({
   auth: authReducer,
   ...
});
Enter fullscreen mode Exit fullscreen mode

This naming convention proves useful when calling another reducer from within a reducer. For instance, the authReducer can embed results from a settingsReducer:

// reducers/auth.js
import settingsReducer from './settings'

function authReducer(state = {}, action) {
  switch (action.type) {
    case SET_AUTH_SETTINGS:
      return {
         ...state, 
         settings: settingsReducer(state.settings, action)
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

3. Nesting Multiple combineReducers:
As your app complexity grows, consider further splitting reducers by nesting them. Expanding upon the auth reducer, combine reducers from its own folder:

// nested reducer setup
reducers/
   account.js
   auth/
      account.js
      index.js
      subscription.js
      security.js
   index.js
   plans.js
   ...
Enter fullscreen mode Exit fullscreen mode

Within reducers/auth/index.js, use another combineReducers call to merge all sub-reducers within the auth/ directory:

// reducers/auth/index.js
import { combineReducers } from 'redux'
import account from './account'
import security from './security'
import subscription from './subscription'

export default combineReducers({
  account,
  security,
  subscription,
});
Enter fullscreen mode Exit fullscreen mode

This nesting approach is relevant to redux-persist as it enables the blacklisting or whitelisting of specific store chunks for persistence. For example, persisting store.auth while blacklisting store.plans is achievable.

Later, we'll explore how to configure these settings within your reducer files for a more organized structure. This modularization facilitates the separation of reducers intended for persistence, allowing developers to annotate files with comments indicating whether a reducer deals with persisted state.

Integrating Redux Persist for Persistent State Management:

Setting Up Redux Persist:
Integrating redux-persist with a Redux store can be accomplished within your App.js file or the preferred location for creating your Redux store.

1. Redux Persist Basic Setup:
Import the necessary components and wrappers from the basic Redux and redux-persist libraries. The primary components include persistStore and persistReducer, while the storage mechanism, in this case, is AsyncStorage. Additionally, import autoMergeLevel2 for state reconciliation.

// basic redux boilerplate
import { createStore } from 'redux'
import { Provider } from 'react-redux'
// redux-persist wrappers
import { persistStore, persistReducer } from 'redux-persist'
// local storage for persisting data
import AsyncStorage from '@react-native-community/async-storage'
// redux-persist merge level
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'
// root reducer - reducers/index.js
import rootReducer from './reducers'
// component for wrapping the component tree
import { PersistGate } from 'redux-persist/lib/integration/react';
Enter fullscreen mode Exit fullscreen mode

2. Configuration and Wrapping:
Configure redux-persist using the persistConfig object. This object includes the key, storage, and state reconciler. Wrap the root reducer with persistReducer and create the store and persistor. Use the PersistGate component to wrap your component tree below the Redux <Provider>.

// persist config
const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
  stateReconciler: autoMergeLevel2
};
// wrap persist API around root reducer and store
const persistedReducer = persistReducer(persistConfig, rootReducer);

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

// App component
const App = () => {
  return (
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <RootComponent />
      </PersistGate>
    </Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Ensure to provide required props, such as loading and persistor for <PersistGate>. If no preloader is desired, pass loading a value of null.

3. Working with Blacklist and Whitelist:
To include or omit parts of the store for persistence, use the blacklist or whitelist properties within the persistConfig object. Define arrays of reducer keys to be omitted or included in AsyncStorage.

// defining a blacklist
const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
  stateReconciler: autoMergeLevel2,
  blacklist: ['plans', 'session'],
};
Enter fullscreen mode Exit fullscreen mode

When integrating redux-persist into an existing project, consider using the whitelist property to gradually migrate persisted state from a manual setup.

4. Omitting Properties from Nested Reducers:
To exclude properties from nested reducers, define a persistConfig object within the respective nested reducer file. Wrap the reducer with persistReducer at the time of exporting.

// reducers/auth/index.js
import { combineReducers } from 'redux'
import account from './account'
import security from './security'
import subscription from './subscription'
import AsyncStorage from '@react-native-community/async-storage'
import { persistReducer } from 'redux-persist';

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

const authReducer = combineReducers({
  account,
  security,
  subscription,
});

export default persistReducer(persistConfig, authReducer);
Enter fullscreen mode Exit fullscreen mode

Organizing persistConfig Objects:
Decide whether to define persistConfig objects in each reducer file or in one central location, like the root reducer file. Both approaches are viable; choose based on code organization preferences.

Example: Defining persistConfig in the root reducer file:

// reducers/index.js
...
const rootPersistConfig = {
  key: 'root',
  storage: AsyncStorage,
};

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

const rootReducer = combineReducers({
   account,
   auth: persistReducer(authPersistConfig, authReducer),
   ...
});

export default persistReducer(rootPersistConfig, rootReducer);
Enter fullscreen mode Exit fullscreen mode

A Note on Transforms:
Transforms in redux-persist allow customization of the state object during persistence and rehydration. Although an advanced topic, transforms offer options like the compress transform, minimizing state before persisting it to local storage. Explore the official documentation on GitHub for more details and capabilities.

For further reading about transforms, refer to the official documentation on GitHub for a comprehensive list and detailed explanations of their capabilities.


Similar to this, I run a developer-centric community on Slack. Where we discuss these kinds of topics, implementations, integrations, some truth bombs, lunatic chats, virtual meets, and everything that will help a developer remain sane ;) Afterall, too much knowledge can be dangerous too.

I'm inviting you to join our free community, take part in discussions, and share your freaking experience & expertise. You can fill out this form, and a Slack invite will ring your email in a few days. We have amazing folks from some of the great companies (Atlassian, Gong, Scaler etc), and you wouldn't wanna miss interacting with them. Invite Form

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .