redux-persist: writing Redux state to storage and rehydrating it on the next launch
persist and rehydrate a redux store
At a glance
- What is it?
- redux-persist wraps a Redux root reducer to write state to localStorage, AsyncStorage or any pluggable backend as it changes, then loads it back when the app restarts. v7 is in beta and requires Redux 5 or Redux Toolkit 2; the stable npm channel still ships v6.
- Who is it for?
- redux-persist is the right tool when local Redux state needs to survive a page reload and the storage backend is already chosen. It is the wrong tool when state must sync with a server, when the data belongs in a database, or when offline-first conflict resolution is the requirement.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Keeping Redux state alive after a page reload
Redux stores everything in memory. Close the browser tab, reload the page, or restart the app, and the store reinitializes from reducer defaults. Any state not mirrored to a server disappears. Applications that keep user preferences, shopping carts, draft forms, or session tokens in Redux need to write that storage layer themselves, or use redux-persist.
After setup, the persistor subscribes to the store and serializes changed state to the configured backend. On the next startup, it reads those saved values and dispatches a rehydration action that replaces the default-initialized slices with what was stored. Components that mount before that action completes render with reducer defaults rather than stored values, which is the exact case PersistGate is designed to handle.
The scope is narrow: redux-persist handles local Redux state and nothing else. API responses, server state, and anything that arrives over the network fall outside its contract. Syncing stored local state against what a backend now holds is application logic, and that work happens after rehydration, not during it.
persistReducer wraps the reducer; persistStore creates the persistor
Two functions cover the entire setup. `persistReducer` takes a config object and the root reducer, and returns an augmented reducer that appends a `_persist` key to state for internal bookkeeping. `persistStore` takes the resulting store and returns a persistor, which issues the initial load from storage and exposes a `flush()` method for writing state to disk immediately, rather than waiting for the next dispatch.
The README's Quickstart shows both in a Redux Toolkit project:
// app/store.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/storage' // localStorage on the web
import userReducer from './features/user/userSlice'
import configReducer from './features/config/configSlice'
const rootReducer = combineReducers({
user: userReducer,
config: configReducer
})
const persistConfig = {
key: 'root',
version: 1,
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = configureStore({
reducer: persistedReducer,
})
export const persistor = persistStore(store)The config object has 3 required fields: `key`, `version` and `storage`. The `version` integer feeds the migrations system. Nothing in the component layer changes: dispatch, connect, and useSelector all work the same way as without the wrapper.
The Quickstart install command is `npm install redux-persist`, which gives v6 from the stable npm channel; the README's top callout says v7 is in beta, requires `npm install redux-persist@next`, and needs Redux 5 or Redux Toolkit 2. The library is MIT licensed; v7.0.0-beta.1 is the current beta, and the last push to master was on 2026-10-04.
PersistGate blocks rendering until stored state loads
When a React app starts, the store exists before storage finishes loading the previous session's data. Without coordination, components read initial-state defaults for a moment before the rehydration action fires and populates the store. `PersistGate` addresses this by not rendering its children until the rehydration action has fired and the store reflects stored values.
import { PersistGate } from 'redux-persist/react'
import { store, persistor } from './app/store'
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootComponent />
</PersistGate>
</Provider>
);
};The `loading` prop accepts `null` or any React element. A spinner or skeleton screen goes there while storage loads. When blocking render is not acceptable, `useRehydrated(persistor)` from `redux-persist/react` returns a boolean, and the component decides what to render.
State reconciler behavior kicks in once rehydration fires. The default strategy merges one level deep: stored top-level keys replace their initial-state counterparts entirely. Nested keys inside a stored slice are not merged with defaults from the current code, so a new nested field added in a later deploy is absent from any rehydrated session. That problem does not surface as an error. The README directs developers to `docs/state-reconciler.md` before shipping, because a different reconciler may be needed for stores with deep nesting.
localStorage ships in the box; AsyncStorage and IndexedDB need an adapter
The import `redux-persist/storage` gives localStorage on the web. For sessions that should not outlive the current tab, the package includes a sessionStorage adapter. For React Native, no backend comes in the package; the config takes an explicit storage engine:
import AsyncStorage from '@react-native-async-storage/async-storage'
const persistConfig = {
key: 'root',
storage: AsyncStorage,
}A storage engine list at `docs/storage-engines.md` covers community adapters for IndexedDB, filesystem-based backends, and other targets. The storage interface is open: any adapter that implements it works as a drop-in.
Server-side rendering requires no extra configuration because `redux-persist/storage` stores nothing on the server; stored state loads only in the browser. For Next.js projects using the App Router, `PersistGate` must be rendered from a Client Component, because React Server Components run server-side where the storage backend is inactive. The PersistGate documentation at `docs/PersistGate.md` covers that case with a specific example.
Each backend has a different durability guarantee. localStorage survives browser restarts, while sessionStorage does not survive a tab close and AsyncStorage on React Native is scoped to the individual app install. Using the wrong backend gives the correct API surface with the wrong lifetime, and the mismatch is silent until a user's data disappears.
denylist and allowlist pick which top-level state keys are saved
The persist config does not have to save everything. Slices that initialize correctly from reducer defaults, such as navigation state and loading flags, are candidates for exclusion. The `denylist` and `allowlist` config keys choose which top-level keys are excluded or included:
persistReducer({ key: 'root', storage, denylist: ['navigation'] }, rootReducer)
// navigation will not be persisted
persistReducer({ key: 'root', storage, allowlist: ['navigation'] }, rootReducer)
// only navigation will be persistedIn TypeScript, these arrays are checked against the state shape. A key name absent from the state type becomes a compile error when the config uses `PersistConfig<RootState>` or `as const` on the array. The config also accepts `blacklist` as a synonym for `denylist`, and `whitelist` as a synonym for `allowlist`.
Both options operate at the top level of the state tree only. Going one level deeper, giving separate slices their own storage backends, or splitting migration histories across a code-split app requires nested persists, documented at `docs/nested-persists.md`. Each nested slice carries its own `persistReducer` call and config, so a large app can persist user settings to localStorage and an offline cache to IndexedDB in the same store, with separate migration sequences for each part.
Migrations handle mismatches when the stored state shape changes
The `version` integer in the persist config identifies the data format that is currently on disk. Rename a key in a reducer and the value on disk no longer maps to the current state tree. Split a slice into two, and the old flat structure cannot unpack correctly. A migration function receives the stored state object and returns a transformed version; the `version` integer is how the library knows which functions to call and in what order.
On rehydration, the library compares the version in the stored data against the version in the config. When they differ, migration functions for each intervening version run in sequence, transforming stored state from its old format to the current one. If no migration exists for a gap, the library falls back to the purge behavior in the config, which by default discards stale data and starts from initial state.
The Quickstart uses `version: 1` as the starting point. Incrementing that integer on the next deploy, alongside a migration function, lets existing sessions transform their stored data rather than lose it. The Learn more section in the README links to 11 documentation pages; `docs/migrations.md` covers the migration API and `docs/MigrationGuide-v7.md` covers the upgrade path from the prior release. Issue #809 reports a separate case where stored data was wiped outside of migration flows, and the README notes that case is addressed.
Resetting state on logout must happen inside the persisted reducer
The README documents a specific failure mode for logout flows. Redux applications commonly reset the store by returning `undefined` from the root reducer on a logout action, which causes all reducers to fall back to their initial state. With redux-persist, that approach breaks when the reset happens above the `persistReducer` boundary.
Wrapping the persisted reducer or using a store enhancer that replaces the whole state removes the `_persist` key. Without that key, state stops being saved to storage. On the next launch, the previous user's state reloads from disk instead of the reset state. The library logs an error in development when this happens, which is the primary signal that the wrong reset pattern is in use.
Placing the reset inside the reducer that is passed to `persistReducer` is the documented fix:
const rootReducer = (state, action) =>
appReducer(action.type === 'user/logout' ? undefined : state, action)
const persistedReducer = persistReducer(persistConfig, rootReducer)After dispatching the logout action, calling `await persistor.flush()` writes the reset state to storage immediately. Without the flush, the reset may not reach disk before the app closes. The README notes that flush is the right call before the app might close, because the reset state must be committed to prevent the previous session from reloading.
Editorial conclusion
redux-persist is the right tool when local Redux state needs to survive a page reload and the storage backend is already chosen. It is the wrong tool when state must sync with a server, when the data belongs in a database, or when offline-first conflict resolution is the requirement. Before shipping, read docs/state-reconciler.md: the default one-level merge discards nested defaults silently and the mismatch surfaces as stale values, not errors. Read the README's opening callout before running the install command, because it explains which install path corresponds to which documentation.
Frequently asked questions
What does redux-persist do?
redux-persist intercepts Redux state changes, writes the selected slices to a storage backend, and reads them back into the store on the next launch. The loading step is called rehydration. Built-in adapters cover localStorage and sessionStorage, with community adapters for AsyncStorage and IndexedDB.
How do I add redux-persist to a Redux project?
Install the package from npm with npm install redux-persist. Then wrap the root reducer with persistReducer using a config that includes key, version and storage fields, and call persistStore on the resulting store to create the persistor.
Does redux-persist work with React Native?
Yes, but no storage backend ships in the package for React Native. The config requires an explicit storage engine, and the README shows passing AsyncStorage from @react-native-async-storage/async-storage.
What is PersistGate in redux-persist?
PersistGate is a React component imported from redux-persist/react that blocks rendering until stored state has loaded into the store. Its loading prop accepts null or any React element to show while waiting. An alternative hook, useRehydrated(persistor), returns a boolean instead of blocking.
What happens to stored state when a reducer's initial state adds new nested keys?
With the default reconciler, rehydration merges one level deep: stored top-level keys replace their initial-state counterparts entirely. A new nested key that exists in initial state but not in stored data is discarded rather than filled in. The docs/state-reconciler.md page covers how to choose a different reconciler.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/endograph-redux-persist)