rtk-persist
is a lightweight, zero-dependency library that enhances Redux Toolkit's state management by adding seamless, persistent storage. It allows specified slices or reducers of your Redux state to be saved to a storage medium of your choice (like localStorage
or AsyncStorage
) and rehydrated on app startup.
The library works by wrapping standard Redux Toolkit functions, adding persistence logic without changing the way you write your reducers or actions.
-
Effortless Persistence: Persist any Redux Toolkit slice or reducer with minimal configuration.
-
Asynchronous Rehydration: Store creation is now asynchronous, ensuring that your app only renders after the state has been fully rehydrated.
-
Seamless Integration: Designed as a drop-in replacement for RTK functions. Adding or removing persistence is as simple as changing an import.
-
React Redux Integration: Comes with a
<PersistedProvider />
and ausePersistedStore
hook for easy integration with React applications. -
Flexible API: Choose between a
createPersistedSlice
utility or acreatePersistedReducer
builder syntax. -
Nested State Support: Easily persist slices or reducers that are deeply nested within your root state using a simple
nestedPath
option. -
Custom Serialization: Use
onPersist
andonRehydrate
to transform your state before saving and after loading. -
Storage Agnostic: Works with any storage provider that implements a simple
getItem
,setItem
, andremoveItem
interface. -
TypeScript Support: Fully typed to ensure a great developer experience with path validation.
-
Minimal Footprint: Extremely lightweight with a production size under 15 KB.
You can install rtk-persist
using either yarn
or npm
:
yarn add rtk-persist
or
npm install --save rtk-persist
The package has a peer dependency on @reduxjs/toolkit
and react-redux
if you use the React integration.
rtk-persist
offers two ways to make your state persistent. Both require using configurePersistedStore
in your store setup.
This approach is best if you prefer the createSlice
API from Redux Toolkit.
Replace createSlice
with createPersistedSlice
. The function accepts the same options.
// features/counter/counterSlice.ts
import { createPersistedSlice } from 'rtk-persist';
import { PayloadAction } from '@reduxjs/toolkit';
export const counterSlice = createPersistedSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
This approach is ideal if you prefer the createReducer
builder syntax.
Use createPersistedReducer
and define your case reducers using the builder callback.
// features/counter/counterReducer.ts
import { createPersistedReducer } from 'rtk-persist';
import { createAction } from '@reduxjs/toolkit';
const increment = createAction<number>('increment');
const decrement = createAction<number>('decrement');
export const counterReducer = createPersistedReducer(
'counter', // A unique name for the reducer
{ value: 0 }, // Initial state
(builder) => {
builder
.addCase(increment, (state, action) => {
state.value += action.payload;
})
.addCase(decrement, (state, action) => {
state.value -= action.payload;
});
}
);
Whichever option you choose, you must use configurePersistedStore
and provide a storage handler. The store creation is asynchronous and returns a promise that resolves with the rehydrated store.
// app/store.ts
import { configurePersistedStore } from 'rtk-persist';
import { counterSlice } from '../features/counter/counterSlice';
// import { counterReducer } from '../features/counter/counterReducer';
// For web, use localStorage or sessionStorage
const storage = localStorage;
export const store = configurePersistedStore(
{
reducer: {
// IMPORTANT: The key must match the slice's `name` or the reducer's `name`.
[counterSlice.name]: counterSlice.reducer,
// [counterReducer.reducerName]: counterReducer,
},
},
'my-app-id', // A unique ID for your application
storage
);
// Note: RootState and AppDispatch types need to be inferred differently
// due to the asynchronous nature of the store.
// This is typically handled within your React application setup.
export type Store = Awaited<typeof store>;
export type RootState = ReturnType<Store['getState']>;
export type AppDispatch = Store['dispatch'];
For React applications, rtk-persist
provides a PersistedProvider
and a usePersistedStore
hook to make integration seamless.
This component replaces the standard Provider
from react-redux
. It waits for the store to be rehydrated before rendering your application, preventing any flicker of initial state.
In your application's entry point (e.g., main.tsx
or index.js
), wrap your App
component with PersistedProvider
.
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { PersistedProvider } from 'rtk-persist';
import { store } from './state/store'; // This is the promise from configurePersistedStore
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<PersistedProvider store={store} loader={<div>Loading...</div>}>
<App />
</PersistedProvider>
</React.StrictMode>,
);
The PersistedProvider
accepts two props:
-
store
: The promise returned byconfigurePersistedStore
. -
loader
(optional): A React node to display while the store is rehydrating.
A custom hook that provides access to the rehydrated store instance. This is useful for dispatching actions or accessing store methods.
import React from 'react';
import { usePersistedStore } from 'rtk-persist';
const MyComponent = () => {
const { store } = usePersistedStore();
const handleClear = () => {
// Manually clears the persisted state from storage.
store.clearPersistedState();
};
return <button onClick={handleClear}>Clear Persisted State</button>;
};
If your persisted slice or reducer is not at the root of your state object, you must provide a nestedPath
to ensure it can be found for persistence and rehydration.
The nestedPath
is a dot-notation string representing the path from the root of the state to the slice.
Imagine your state is structured like { features: { counter: { value: 0 } } }
. Here's how you would configure the counter slice:
// features/counter/counterSlice.ts
export const counterSlice = createPersistedSlice(
{
name: 'counter',
initialState: { value: 0 },
reducers: {
/* ... */
},
},
{
nestedPath: 'features.counter' // The nestedPath to the slice's state
}
);
// app/store.ts
import { combineReducers } from '@reduxjs/toolkit';
import { configurePersistedStore } from 'rtk-persist';
import { counterSlice } from '../features/counter/counterSlice';
const featuresReducer = combineReducers({
[counterSlice.name]: counterSlice.reducer,
});
export const store = configurePersistedStore(
{
reducer: {
features: featuresReducer,
},
},
'my-app-id',
localStorage
);
Sometimes, you may need to transform a slice's state before it's saved to storage or after it's rehydrated. For example, you might want to store a Date
object as an ISO string, or omit certain transient properties.
rtk-persist
supports this through the onPersist
and onRehydrate
options.
Here's how you can persist a slice that contains a non-serializable value like a Date
object.
// features/session/sessionSlice.ts
import { createPersistedSlice } from 'rtk-persist';
interface SessionState {
lastLogin: Date | null;
token: string | null;
}
const initialState: SessionState = {
lastLogin: null,
token: null,
};
export const sessionSlice = createPersistedSlice(
{
name: 'session',
initialState,
reducers: {
login: (state, action) => {
state.token = action.payload.token;
state.lastLogin = new Date();
},
logout: (state) => {
state.token = null;
state.lastLogin = null;
},
},
},
{
// Transform state before saving
onPersist: (state) => ({
...state,
lastLogin: state.lastLogin ? state.lastLogin.toISOString() : null,
}),
// Transform state after rehydrating
onRehydrate: (state) => ({
...state,
lastLogin: state.lastLogin ? new Date(state.lastLogin) : null,
}),
}
);
In this example:
-
onPersist
converts thelastLogin
Date
object into an ISO string before it's written tolocalStorage
. -
onRehydrate
parses the ISO string and converts it back into aDate
object when the state is loaded from storage.
A wrapper around RTK's createSlice
that adds persistence.
-
sliceOptions
: The standardCreateSliceOptions
object from Redux Toolkit. -
persistenceOptions
(optional,object
): Configuration for persistence behavior.-
nestedPath
(optional,string
): A dot-notation string for the slice's state if it's nested. -
onPersist
(optional,function
): A function to transform state before it's saved. -
onRehydrate
(optional,function
): A function to transform state after it's rehydrated.
-
- A
PersistedSlice
object, which is a standardSlice
object enhanced with persistence properties.
A wrapper around RTK's createReducer
that adds persistence.
-
name
: A unique string to identify this reducer in storage. -
initialState
: The initial state for the reducer. -
builderCallback
: A callback that receives abuilder
object to define case reducers. -
persistenceOptions
(optional,object
): Configuration for persistence behavior.-
nestedPath
(optional,string
): A dot-notation string for the reducer's state. An empty string (''
) signifies the root state. -
onPersist
(optional,function
): A function to transform state before it's saved. -
onRehydrate
(optional,function
): A function to transform state after it's rehydrated.
-
- A
PersistedReducer
function, which is a standardReducer
enhanced with persistence properties.
A wrapper around RTK's configureStore
.
-
storeOptions
: The standardConfigureStoreOptions
object. -
applicationId
: A unique string that identifies the application to namespace storage keys. -
storageHandler
: A storage object that implementsgetItem
,setItem
, andremoveItem
. -
persistenceOptions
(optional): An object to control the persistence behavior:rehydrationTimeout
(optional,number
): Max time in ms to wait for rehydration. Defaults to5000
.
-
A
Promise<PersistedStore>
object, which resolves to a standard Redux store enhanced with the following methods:-
rehydrate()
: A function to manually trigger rehydration from storage. -
clearPersistedState()
: A function that clears all persisted data for the application from storage.
-
This library is authored and maintained by Fancy Pixel.
This library was crafted from our daily experiences building modern web and mobile applications. Contributions are welcome!
This project is licensed under the MIT License.
Library icon freely created from a iconsax icon and the redux logo.