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 ausePersistedStorehook for easy integration with React applications. -
Flexible API: Choose between a
createPersistedSliceutility or acreatePersistedReducerbuilder syntax. -
Nested State Support: Easily persist slices or reducers that are deeply nested within your root state using a simple
nestedPathoption. -
Custom Serialization: Use
onPersistandonRehydrateto transform your state before saving and after loading. -
Storage Agnostic: Works with any storage provider that implements a simple
getItem,setItem, andremoveIteminterface. -
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-persistor
npm install --save rtk-persistThe 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:
-
onPersistconverts thelastLoginDateobject into an ISO string before it's written tolocalStorage. -
onRehydrateparses the ISO string and converts it back into aDateobject when the state is loaded from storage.
A wrapper around RTK's createSlice that adds persistence.
-
sliceOptions: The standardCreateSliceOptionsobject 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
PersistedSliceobject, which is a standardSliceobject 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 abuilderobject 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
PersistedReducerfunction, which is a standardReducerenhanced with persistence properties.
A wrapper around RTK's configureStore.
-
storeOptions: The standardConfigureStoreOptionsobject. -
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.