Persist and rehydrate a redux store.
Redux state lives entirely in memory and resets on every page refresh or app restart. redux-persist solves this by automatically saving your Redux store to a storage engine (such as localStorage on web or AsyncStorage on React Native) and restoring it when the application reloads — with no changes required to your existing reducers or actions.
Beyond basic persistence, the library also handles state shape migrations as your app evolves, lets you choose exactly which slices of state to save via whitelists and blacklists, and provides a PersistGate React component to delay rendering until rehydration is complete.
For a deeper look at the problem this project solves and why it was built, see docs/project-purpose.md.
Note on versioning: The initial
v0.0.1npm release was a proof-of-concept publish to establish the@mdemichele/redux-persistpackage name. The first real release wasv6.1.0, aligning with the actual version of the codebase (the TypeScript fork) and providing a meaningful starting point for future semantic versioning.
- June 9, 2026: v0.0.1 released to npm as
@mdemichele/redux-persist. Following a regular release cadence from here on. - February 16, 2025: New Fork Created. I'm hoping we can revive this project and get it actively maintained again.
- October 15th, 2021: Move to TypeScript (Thanks @smellman)
- As part of the work to upgrade the infrastructure used to build redux-persist, we're moving from Flow to TypeScript.
- Move from TravisCI to GitHub Actions (.github/workflows/ci.yml)
- Version updates for some dependencies
- September 22nd, 2021 - Under New Management
- July 22nd, 2015: Project originally created by @rt2zz
npm install @mdemichele/redux-persist
Usage Examples:
- Basic Usage
- Nested Persists
- Hot Module Replacement
- Code Splitting [coming soon]
For a working reference implementation, see the redux-persist-test-app example app.
There are two required steps:
- Wrap your root reducer with
persistReducerand a configuration object. - Call
persistStoreon the resulting store.
// configureStore.js
import { createStore } from 'redux'
import { persistStore, persistReducer } from '@mdemichele/redux-persist'
import storage from '@mdemichele/redux-persist/storage' // defaults to localStorage for web
import rootReducer from './reducers'
const persistConfig = {
key: 'root', // the key used to store state in storage
storage, // the storage engine to use
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export default () => {
let store = createStore(persistedReducer)
let persistor = persistStore(store)
return { store, persistor }
}persistConfig requires two fields at minimum:
key— the storage key under which the entire persisted state is stored. Using'root'is conventional for top-level persistence.storage— the storage engine. The default import (@mdemichele/redux-persist/storage) useslocalStorageon web. See Storage Engines for other options.
persistReducer(config, reducer) returns an enhanced reducer that handles the PERSIST, REHYDRATE, and PURGE actions automatically. Swap it in place of your original root reducer — no other changes to your reducer logic are needed.
persistStore(store) returns a Persistor object that drives the persistence lifecycle. It immediately begins reading from storage and dispatches a REHYDRATE action once the stored state is loaded. The Persistor also exposes methods for pausing, resuming, flushing, and purging persistence — see the API section for details.
Important: Every app needs to choose how many levels of state to merge when rehydrated data is loaded back in. The default (
autoMergeLevel1) performs a shallow one-level merge. Read through the State Reconciler section to choose the right option for your app.
React: wrapping your app with PersistGate
Because rehydration is asynchronous, your app may briefly render with default state before persisted data arrives. Wrap your root component with PersistGate to hold rendering until rehydration is complete.
// App.js
import { PersistGate } from '@mdemichele/redux-persist/integration/react'
import { store, persistor } from './configureStore'
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootComponent />
</PersistGate>
</Provider>
);
};- The
loadingprop is rendered while rehydration is in progress. Passnullto render nothing, or a loading component such asloading={<LoadingScreen />}. PersistGatealso accepts a function as children: the function receives a singlebootstrappedboolean argument and is re-invoked once persistence is complete, which is useful for adding transition animations.
For the complete API reference see docs/api.md.
Wraps a reducer so that it automatically persists and rehydrates state.
configobject — required fields:key,storage. See the full config options below.reducerfunction — any reducer, typically the root reducer returned bycombineReducers.- Returns an enhanced reducer. Swap this in place of your original reducer when calling
createStore.
persistConfig options:
| Option | Type | Default | Description |
|---|---|---|---|
key |
string |
— | Required. The key used to store state in the storage engine. |
storage |
object |
— | Required. The storage engine (see Storage Engines). |
version |
number |
-1 |
Integer version of your state shape. Used with createMigrate to run migrations. |
whitelist |
string[] |
— | Only these reducer keys will be persisted. All others are ignored. |
blacklist |
string[] |
— | These reducer keys will not be persisted. All others are saved. |
transforms |
Transform[] |
— | Functions to transform state before writing to or after reading from storage. |
throttle |
number |
— | Milliseconds to throttle write calls to the storage engine. |
migrate |
function |
— | Custom migration function. Receives stored state and returns a promise of new state. |
stateReconciler |
function | false |
autoMergeLevel1 |
Controls how rehydrated state is merged. Pass false to disable reconciliation. |
serialize |
boolean |
true |
Set to false to skip JSON.stringify/JSON.parse during storage operations. |
timeout |
number |
5000 |
Milliseconds to wait for storage to resolve before aborting. Useful for React Native. |
debug |
boolean |
false |
Set to true to enable verbose logging. |
writeFailHandler |
function |
— | Called with the error if the storage engine fails during setItem. Useful for detecting quota exhaustion. |
Begins the persistence lifecycle and returns a Persistor.
storeredux store — the store returned bycreateStorewith yourpersistedReducer.configobject (optional)manualPersist: true— prevents persistence from starting automatically. Callpersistor.persist()manually when your storage is ready. Useful when the storage engine is not available at startup (e.g. on certain React Native environments).
callbackfunction (optional) — called once the initial rehydration is complete.- Returns a Persistor object.
The Persistor is a small Redux store that drives the persistence lifecycle. It exposes the following methods:
.persist()— starts or resumes persistence. Called automatically unlessmanualPersistis set..pause()— pauses persistence. State changes will not be written to storage while paused..flush()— immediately writes all pending state to storage and returns a promise. Useful before app close or logout..purge()— removes all persisted state from storage and returns a promise. Note: this only clears storage — it does not reset the in-memory Redux state.
When your app loads, redux-persist reads the previously saved state from storage and merges it back into the Redux store. A state reconciler is the function that controls exactly how that merge happens — specifically, how the rehydrated (stored) state is combined with the reducer's current initial state.
This matters because your reducer's initial state may have changed since the last time the user ran the app (e.g. you added a new field). The reconciler decides whether to keep the stored value, use the new initial value, or merge the two.
There are three reconcilers available out of the box:
1. autoMergeLevel1 (default)
// No import needed — this is the default behaviorPerforms a shallow merge one level deep. For each top-level key in the stored state:
- If the reducer has already modified that key during the
REHYDRATEaction, the stored value is skipped (the reducer's value wins). - Otherwise, the stored value overwrites the initial state value.
Keys that exist in initial state but not in stored state are preserved.
incoming state: { foo: incomingFoo }
initial state: { foo: initialFoo, bar: initialBar }
reconciled state: { foo: incomingFoo, bar: initialBar }
Best for: Most apps. Safe default that preserves new reducer keys while restoring previously saved values.
2. autoMergeLevel2
import autoMergeLevel2 from '@mdemichele/redux-persist/lib/stateReconciler/autoMergeLevel2'Behaves like autoMergeLevel1 but goes one level deeper: if a top-level key holds a plain object, the stored and initial values for that object are themselves shallow-merged rather than the stored value simply overwriting the initial.
incoming state: { foo: { a: 1 } }
initial state: { foo: { a: 0, b: 2 }, bar: initialBar }
reconciled state: { foo: { a: 1, b: 2 }, bar: initialBar }
Best for: Apps where top-level state keys hold objects with multiple sub-fields, and you want new sub-fields from the reducer's initial state to survive rehydration.
3. hardSet
import hardSet from '@mdemichele/redux-persist/lib/stateReconciler/hardSet'Replaces state entirely with the stored (inbound) state. No merging occurs. Keys that exist in the initial state but not in stored state are dropped.
incoming state: { foo: incomingFoo }
initial state: { foo: initialFoo, bar: initialBar }
reconciled state: { foo: incomingFoo } // bar is dropped
Best for: Nested persistReducer configurations, or reducers that do not rely on initialState at all. Use with caution at the root level — new state keys added to your reducer will be missing until the user clears storage.
Configuring a reconciler:
import autoMergeLevel2 from '@mdemichele/redux-persist/lib/stateReconciler/autoMergeLevel2'
const persistConfig = {
key: 'root',
storage,
stateReconciler: autoMergeLevel2,
}Pass stateReconciler: false to disable reconciliation entirely — the stored state will be returned as-is with no merging.
See the Basic Usage section above for a full walkthrough of PersistGate. In summary, PersistGate delays rendering your app until rehydration is complete, and supports two modes:
loadingprop — renders the provided value (ornull) while rehydration is in progress, then renders children.- Function children — passes a
bootstrappedboolean to a render function, giving you control over transitions and animations.
// Mode 1: loading prop
<PersistGate loading={<LoadingScreen />} persistor={persistor}>
<RootComponent />
</PersistGate>
// Mode 2: function children
<PersistGate persistor={persistor}>
{(bootstrapped) => bootstrapped ? <RootComponent /> : <LoadingScreen />}
</PersistGate>By default, redux-persist saves every key in your root reducer. Use blacklist or whitelist in your persistConfig to control which slices of state are persisted.
blacklist— persist everything except the listed keys.whitelist— persist only the listed keys.
// Persist everything except ‘navigation’
const persistConfig = {
key: ‘root’,
storage,
blacklist: [‘navigation’],
};
// Persist only ‘auth’ and ‘userPreferences’
const persistConfig = {
key: ‘root’,
storage,
whitelist: [‘auth’, ‘userPreferences’],
};Note:
blacklistandwhitelistonly filter at the top level of your state tree. To filter keys nested deeper than one level, use Nested Persists.
Nesting a persistReducer inside another persistReducer gives you independent persistence configuration for different parts of your state tree. Common use cases include:
- Deep filtering —
blacklist/whitelistonly work one level deep, but a nested persist lets you exclude specific sub-keys. - Different storage engines — persist sensitive data (e.g. auth tokens) to a secure store while keeping the rest in
localStorage. - Code splitting — configure persistence independently for lazily loaded reducers.
In the example below, the root config excludes the entire auth slice from its own persistence, and a separate config for auth handles its own persistence — excluding just the somethingTemporary sub-key.
import { combineReducers } from ‘redux’
import { persistReducer } from ‘@mdemichele/redux-persist’
import storage from ‘@mdemichele/redux-persist/storage’
import { authReducer, otherReducer } from ‘./reducers’
const rootPersistConfig = {
key: ‘root’,
storage,
blacklist: [‘auth’], // auth is excluded here — it manages its own persistence below
}
const authPersistConfig = {
key: ‘auth’,
storage,
blacklist: [‘somethingTemporary’], // only this sub-key is excluded from auth persistence
}
const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer),
other: otherReducer,
})
export default persistReducer(rootPersistConfig, rootReducer)As your app evolves, the shape of your Redux state may change — you might rename a key, remove a field, or restructure a slice entirely. Users who already have data persisted under the old shape need a migration path so their stored state remains valid.
persistReducer accepts a migrate config option — a function that receives the stored state and the current version number, and returns a promise resolving to the migrated state. It runs after reading from storage but before state reconciliation.
Redux Persist ships with createMigrate to help write these migrations in a structured way:
import { createMigrate } from ‘@mdemichele/redux-persist’
const migrations = {
0: (state) => {
// version 0: initial shape, no migration needed
return state
},
1: (state) => {
// version 1: ‘userInfo’ was renamed to ‘user’
return {
...state,
user: state.userInfo,
userInfo: undefined,
}
},
}
const persistConfig = {
key: ‘root’,
storage,
version: 1,
migrate: createMigrate(migrations, { debug: false }),
}createMigrate runs all migrations between the stored version and the current version number in sequence. For more advanced usage (async migrations, error handling), pass your own function to migrate directly.
For further details see docs/migrations.md.
Transforms let you intercept and modify state as it is written to storage (inbound) or read back from storage (outbound). This is useful when your state contains values that do not serialize cleanly to JSON — such as Set, Map, Date, class instances, or Immutable.js structures.
createTransform takes three arguments:
- Inbound function — called before state is serialized and written to storage. Receives
(subState, key). - Outbound function — called after state is read from storage, before it is rehydrated into the store. Receives
(subState, key). - Config object — use
whitelistorblacklistto limit which reducer keys the transform applies to. Without this, the transform runs for every persisted key.
Example: persisting a Set
JSON.stringify converts a Set to an empty object {}, losing all data. This transform converts it to an array for storage and back to a Set on rehydration:
import { createTransform } from ‘@mdemichele/redux-persist’
const SetTransform = createTransform(
(inboundState, key) => ({
...inboundState,
mySet: [...inboundState.mySet], // Set → Array before storage
}),
(outboundState, key) => ({
...outboundState,
mySet: new Set(outboundState.mySet), // Array → Set after rehydration
}),
{ whitelist: [‘someReducer’] }
)
export default SetTransformRegister transforms in your persistConfig:
import storage from ‘@mdemichele/redux-persist/storage’
import { SetTransform } from ‘./transforms’
const persistConfig = {
key: ‘root’,
storage,
transforms: [SetTransform],
}Multiple transforms can be provided — they are applied in array order on the way in, and in reverse order on the way out.
Community transform libraries:
| Library | Description |
|---|---|
| redux-persist-transform-immutable | Support Immutable.js reducers |
| redux-persist-seamless-immutable | Support seamless-immutable reducers |
| redux-persist-transform-compress | Compress serialized state with lz-string |
| redux-persist-transform-encrypt | Encrypt serialized state with AES |
| redux-persist-transform-filter | Persist or rehydrate a subset of state |
| redux-persist-transform-filter-immutable | Subset filtering with Immutable.js support |
| redux-persist-transform-expire | Expire state subsets based on a property |
| redux-persist-expire | More flexible expiry with additional options |
redux-persist ships with two built-in storage engines for web:
import storage from ‘@mdemichele/redux-persist/storage’ // localStorage (default for web)
import storageSession from ‘@mdemichele/redux-persist/storage/session’ // sessionStoragesessionStorage behaves like localStorage but is cleared when the browser tab is closed — useful for session-scoped state that should not survive past the current session.
Writing a custom storage engine:
Any object that implements the following async interface can be used as a storage engine:
const customStorage = {
getItem: (key) => Promise<string | null>,
setItem: (key, value) => Promise<void>,
removeItem: (key) => Promise<void>,
}Community storage engines:
| Engine | Environment | Description | Status |
|---|---|---|---|
| @react-native-async-storage/async-storage | React Native | Official community AsyncStorage — works directly as a storage engine | Active |
| react-native-mmkv | React Native | High-performance key-value storage — requires a thin custom adapter | Active |
| redux-persist-expo-filesystem | React Native (Expo) | Filesystem storage — no linking or ejecting required | Active |
| redux-persist-expo-securestore | React Native (Expo) | Expo SecureStore for sensitive data | Active |
| redux-persist-filesystem-storage | React Native (Android) | Mitigates Android storage size limitations | Active |
| redux-persist-webextension-storage | Chrome / Firefox | Browser extension storage API | Unmaintained |
| redux-persist-cookie-storage | Web / Node.js | Cookie-based storage, works universally | Unmaintained |
| redux-persist-indexeddb-storage | Web | IndexedDB via localForage — recommended for large state | Unmaintained |
| redux-persist-node-storage | Node.js | File-based storage for Node environments | Unmaintained |
| redux-persist-pouchdb | Web / Node.js | PouchDB storage engine | Unmaintained |
| redux-persist-fs-storage | React Native | react-native-fs engine | Unmaintained |
| redux-persist-weapp-storage | WeChat Mini Program | Compatible with wepy | Unmaintained |
| redux-persist-sensitive-storage | React Native | Sensitive data via react-native-sensitive-info | Archived |
| @bankify/redux-persist-realm | React Native | Realm database (requires Realm installation) | Archived |
Contributions are welcome. If you have an outstanding pull request from the original redux-persist repository, please open a new PR here and reference the original — we will review it and work with you to get it integrated. As the codebase has moved to TypeScript, some changes may be needed, but we are happy to help with that.
To report a bug or request a feature, please open an issue.
To submit a contribution:
- Fork the repository and create a branch for your change.
- Run the test suite with
npm testbefore submitting. - Open a pull request with a clear description of what the change does and why.