There is a very handy library called redux-persist that allows you to persist the global state of your application’s Redux store in local storage. This is particularly useful when your JavaScript application breaks on page reload, which often happens during development with hot module replacement.
This article explains how to integrate this library into your web application if you are using the Redux Toolkit. Redux Toolkit is an opinionated toolset for Redux development that provides excellent defaults, and it is my preferred way to set up the Redux store.
Store Setup
First, you need to define the root reducer. Redux Toolkit provides a utility called combineReducers()
, which is well-documented on the official website. The root reducer is then passed to persistReducer
along with a configuration object as the first argument. This function returns a persistedReducer
, which you then pass to the configureStore()
method.
The section on the serializable check fixes an error I haven’t fully understood yet. If the corresponding actions are not ignored in the middleware configuration, the setup still works, but you might encounter console errors. The good news is that the error message links directly to a code example that resolves the issue.
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE, persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { postApi } from '../services/postApi';
import applicationReducer from './applicationSlice';
import postReducer from './postSlice';
const rootReducer = combineReducers({
post: postReducer,
application: applicationReducer,
[postApi.reducerPath]: postApi.reducer,
});
const persistConfig = {
key: 'root',
storage,
blacklist: ['postApi'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(postApi.middleware),
});
setupListeners(store.dispatch);
export default store;
export const persistor = persistStore(store);
Another thing to note is that postApi
is blacklisted in the given code example. When there is no cached data after a page reload, the corresponding query or mutation is executed again. I prefer this behavior because RTK Query‘s caching mechanism is already complex enough, and avoiding extra logic that influences the cache makes the code cleaner.
Adding the Store to the Component Tree
The final step is to integrate the persisted store into the React application. Redux Persist provides the PersistGate
component, which wraps the entire component tree as a child of the Provider
. The store and persistor are passed to their respective components, and that’s it. The Redux store state is persisted in local storage and rehydrates on page reload.
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import App from './App';
import store, { persistor } from './store/store';
export default function Index() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<BrowserRouter>
<Routes>
<Route path={'app/'} element={<App />}>
{/* ... */}
</Route>
</Routes>
</BrowserRouter>
</PersistGate>
</Provider>
);
}