9 min read · Oct 20, 2024
--
Picture this… You’re building an awesome web app — everything’s working perfectly, users are logging in, filling out forms, adding items to their shopping carts,… Your app is smooth, fast, and everyone’s loving it. Then, disaster strikes: Users refresh the page, or worse, they accidentally close the browser. Boooom! All their progress — gone in an instant. The shopping cart they spent an hour curating? Vanished. The long form they were meticulously filling out? Poof. Even their login session is nowhere to be found.
That’ when the Redux Persist enters the game. It stores your app’s data somewhere safe — like LocalStorage, SessionStorage or the mighty IndexedDB — and brings it back every time your app reloads. But which storage option should you choose? Do you go with the simple, reliable LocalStorage, the here-for-a-good-time-not-a-long-time SessionStorage, or the complex, bigger-than-your-data-structure IndexedDB?
In this article, we’ll take a dive into Redux Persist, its key features, and how to pick the right storage medium for your app’s needs.
In most web apps, when a user refreshes the page or closes the browser, the application’s state resets, which can be pretty frustrating. Imagine filling out a long form, and after an accidental refresh, all your data vanishes. Not fun.
Redux Persist solves this problem by allowing your Redux store to persist its state across page reloads or browser sessions. It’s like giving your app a memory, so it doesn’t forget everything the moment the page refreshes.
🤔 Use Cases of Redux Persist
- Forms: Save form data so users can resume filling out a form after closing the browser or navigating away.
- Authentication: Keep users logged in across sessions by persisting authentication tokens.
- Shopping Cart: Retain shopping cart items across page reloads or session terminations.
- Offline Support: Combine Redux Persist with a storage engine like IndexedDB to enable offline-first capabilities.
🔑 Key features of Redux Persist
- State Persistence: It stores your Redux state in a chosen storage (browser or mobile storage) and rehydrates it when the app is reloaded, ensuring state continuity across sessions.
- Rehydration: When your application starts or reloads, Redux Persist will fetch the saved state from the storage and merge it back into your Redux store, providing the same state as before the reload.
- Selective Persistence (Whitelisting and Blacklisting): You can choose which parts of the Redux state should be persisted. You can “whitelist” reducers that need to be saved or “blacklist” reducers that should not be persisted.
- Transforms: You can apply custom transforms to the state before saving it to storage and after retrieving it. This can be useful for things like encrypting the state, trimming down large data objects, or handling date conversions.
- Storage Engines: It supports multiple storage engines like LocalStorage, SessionStorage, IndexedDB, and AsyncStorage (for React Native), as well as custom storage implementations.
- Auto Rehydrate: Once Redux Persist restores the state from storage, it automatically updates the Redux store with the persisted state.
🔨 Set up Redux Persist
- Install
redux-persist
andredux
if you don’t already have them installed:
npm install redux-persist redux
- In your Redux setup, you’ll need to modify the store configuration to enable persistence.
import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
import rootReducer from './reducers'; // your root reducer// Redux Persist configuration
const persistConfig = {
key: 'root', // the key for the persist state in storage
storage, // the storage engine to use (localStorage in this case)
};
// Wrap the root reducer with persistReducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
// Create the Redux store with the persisted reducer
const store = createStore(
persistedReducer
);
// Create a persistor, which will be used to persist and rehydrate the store
const persistor = persistStore(store);
export { store, persistor };
- Use the Persistor in your app. To enable persistence in your app, you need to wrap your application with
PersistGate
provided by Redux Persist. This delays the rendering of your app’s UI until the persisted state has been retrieved and rehydrated.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store'; // your store setup
import App from './App';// Wrap the app with both Provider and PersistGate
ReactDOM.render(
<Provider store={store}>
{/* PersistGate delays the rendering of the app until Redux state is rehydrated */}
<PersistGate loading={<div>Loading...</div>} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root')
);
- Use Whitelisting or Blacklisting Reducers. As mentioned above, you can use
whitelist
orblacklist
in thepersistConfig
to control which parts of your Redux state should persist.
Whitelist: Only the reducers in the whitelist
will be persisted.
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth', 'user'], // only the auth and user reducers will be persisted
};
Blacklist: All reducers will be persisted except the ones in the blacklist
.
const persistConfig = {
key: 'root',
storage,
blackList: ['temporaryData'], // temporaryData won't be persisted
};
- You can also add transforms to the persisted state, such as encrypting sensitive data or trimming large objects before they are saved.
import { createTransform } from 'redux-persist';
import storage from 'redux-persist/lib/storage';// Example transform to encrypt/decrypt sensitive state
const encryptTransform = createTransform(
// Transform the state on its way to being serialized and persisted
(inboundState, key) => {
// Apply encryption or data manipulation here
return encrypt(inboundState); // hypothetical encryption function
},
// Transform the state after it is rehydrated
(outboundState, key) => {
return decrypt(outboundState); // hypothetical decryption function
},
{ whitelist: ['auth'] }
);
const persistConfig = {
key: 'root',
storage,
transforms: [encryptTransform], // use the transform here
};
LocalStorage
LocalStorage is ideal for small to moderate data sets that need to persist across sessions. A synchronous storage mechanism available in browsers stores data in key-value pairs. Data persists even after the browser is closed and reopened.
Usage: redux-persist/lib/storage
import storage from 'redux-persist/lib/storage/session'; // Import sessionStorage instead of localStorageconst persistConfig = {
key: 'root',
storage, // the storage engine (localStorage by default)
};
👍 Pros:
- Simple and easy to use.
- Persistent across sessions (until explicitly deleted).
- Compatible with most modern browsers.
👎 Cons:
- Size limit (around 5–10 MB depending on the browser).
- Synchronous, which can block the main thread on larger data sets.
SessionStorage
SessionStorage is uitable for storing temporary state that doesn’t need to persist across sessions (e.g., form inputs, temporary UI states). Similar to LocalStorage, but data is only stored for the duration of the browser session. Once the user closes the browser, the data is cleared.
Usage: redux-persist/lib/storage/session
import storage from 'redux-persist/lib/storage/session'; // Import sessionStorageconst persistConfig = {
key: 'root',
storage, // defaults to sessionStorage
};
👍 Pros:
- Simple and easy to use.
- Data is cleared when the session ends, preventing long-term data persistence.
👎 Cons:
- Same limitations as LocalStorage regarding size (~5–10 MB).
- Synchronous and can block the main thread for large state data.
IndexedDB
IndexedDB is best for applications with large, complex, or structured state data (e.g., offline-first apps, apps handling large media files). A low-level API for storing large amounts of structured data in the browser. Asynchronous and designed for high performance.
Using Redux Persist with IndexedDB is a powerful combination for persisting the state of your application, especially for web apps with large, complex state or offline capabilities. Here are some key reasons why this setup is so beneficial:
- Redux Persist allows you to automatically save your Redux state to a storage medium such as IndexedDB which is providing persistent storage across the sessions.
- IndexedDB handles large data efficiently unlike LocalStorage or SessionStorage (which have a size limit of around 5MB), IndexedDB is a low-level API that allows storage of much larger amounts of data. It’s designed for handling large datasets, making it ideal for applications that manage heavy state data such as files, media, or complex app states.
- IndexedDB is an asynchronous, non-blocking API, which makes it much faster than synchronous storage options like LocalStorage. This means saving and restoring large chunks of state will not block the main thread, improving the app’s performance and user experience.
- By using Redux Persist with IndexedDB, you can store state even when the user is offline. When the user reconnects, the state can be synced back, enabling seamless offline-first functionality.
- IndexedDB stores data in a structured way (using object stores), allowing you to manage more complex state data. This structured approach is beneficial for larger applications where data integrity is critical, and objects need to be stored and retrieved in a structured manner.
- LocalStorage has relatively strict size limitations and isn’t suited for storing large datasets or binary data. IndexedDB, on the other hand, allows for virtually unlimited storage (depending on the browser), making it ideal for web apps with large amounts of data.
- IndexedDB allows for more custom control over how data is stored and queried, enabling you to handle specific use cases like versioning, updating only parts of the state, or dealing with more complex app structures.
- Modern browsers have extensive support for IndexedDB, making it a reliable choice for persisting app state across multiple platforms, including mobile and desktop.
Usage: Via a third-party library like redux-persist-indexeddb-storage or custom implementation.
🔨 Set up Redux Persist with IndexedDB
- Install
redux-persist
,redux
, andredux-persist-indexeddb-storage
.
npm install redux-persist redux redux-persist-indexeddb-storage
- Configure your Redux store to use IndexedDB as the storage engine
import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import createIndexedDBStorage from 'redux-persist-indexeddb-storage';
import rootReducer from './reducers'; // your root reducer// Create an IndexedDB storage engine
const indexedDBStorage = createIndexedDBStorage('myIndexedDB', 'myDataStore');
// Redux Persist configuration
const persistConfig = {
key: 'root',
storage: indexedDBStorage, // use IndexedDB as storage
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(
persistedReducer,
);
const persistor = persistStore(store);
export { store, persistor };
- Just like with LocalStorage and SessionStorage, you will use
PersistGate
to delay rendering your application until the persisted state from IndexedDB is rehydrated.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store'; // your store setup
import App from './App';// Wrap the app with both Provider and PersistGate
ReactDOM.render(
<Provider store={store}>
{/* PersistGate delays rendering until the persisted state is rehydrated */}
<PersistGate loading={<div>Loading...</div>} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root')
);
👍 Pros:
- Can store large amounts of data (much more than LocalStorage or SessionStorage).
- Asynchronous and non-blocking.
- Supports complex data types (blobs, files, arrays, etc.).
👎 Cons:
- More complex to work with compared to LocalStorage.
- Asynchronous operations can add some complexity to the codebase.
Custom storage
When you need to store state in a non-standard storage medium (e.g., a remote server, database). You can create your own storage mechanism by implementing a custom adapter for specific needs (e.g., saving to a remote server, custom database, etc.).
Usage: Implement your own storage engine by creating a custom object with setItem
, getItem
, and removeItem
methods.
👍 Pros:
- Flexible — can adapt to specific needs (cloud storage, remote databases, etc.).
👎 Cons:
- You are responsible for handling the intricacies of the storage system.
The modern web landscape demands that we prioritize user experience and data integrity. Choosing the right storage option — whether it’s the straightforward LocalStorage, the transient SessionStorage, or the robust IndexedDB — depends on your specific use case and the nature of your application. Each option has its strengths and weaknesses, and understanding them will help you tailor a solution that enhances user satisfaction and engagement.