For a next.js web app, I needed to persist the redux state to the browser's localStorage. In this article, I assume you already know how to setup redux in a next.js app.
Why should I persist the state ?
In this project, there is no database or user session. But still, the user should be able to save and resume its work on each visit to the app.
Demo app
It's a simple ToDo app to demonstrate this technic. You can check it on stackbitz.com here
Todo app stackblitz
Let's see the code
What we need first is two function to serialize
and deserialize
redux state.
app/browser-storage.ts
const KEY = "redux";
export function loadState() {
try {
const serializedState = localStorage.getItem(KEY);
if (!serializedState) return undefined;
return JSON.parse(serializedState);
} catch (e) {
return undefined;
}
}
export async function saveState(state: any) {
try {
const serializedState = JSON.stringify(state);
localStorage.setItem(KEY, serializedState);
} catch (e) {
// Ignore
}
}
Then in pages/_app.tsx
we subscribe to the store so we can save it each time a change happens.
pages/_app.tsx
import "tailwindcss/tailwind.css";
import type { AppProps } from "next/app";
import { Provider } from "react-redux";
import { saveState } from "app/browser-storage";
import { debounce } from "debounce";
import { store } from "app/store";
// here we subscribe to the store changes
store.subscribe(
// we use debounce to save the state once each 800ms
// for better performances in case multiple changes occur in a short time
debounce(() => {
saveState(store.getState());
}, 800)
);
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
The last part is how to restore the saved state when the user comes back. This is done in the store configuration.
We use reduxjs/toolkit configureStore
and it's preloadedState
configuration property.
app/store.ts
import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
import todosReducer from "./feature/todo";
import { loadState } from "./browser-storage";
const reducers = combineReducers({
todos: todosReducer,
});
export const store = configureStore({
devTools: true,
reducer: reducers,
// here we restore the previously persisted state
preloadedState: loadState(),
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
Cons
- Using
localStorage
to persist app's state could impact its performance especially if you have a large state. -
localStorage
is not a bulletproof solution. If the user resets its browser cache app state is lost.
Github
The code is available here github
Notes
No relative imports
You probably noticed that my imports don't use relative paths like ../app/store
but app/store
this is done by configuring your tsconfig.json
with compilerOptions
baseUrl
.
{
"compilerOptions": {
"baseUrl": "."
},
}
Top comments (5)
Dev.to is always very helpful indeed.
But I have a question, can I only save to localStorage ONE STATE SLICE rather than an entire redux store? I think storing an entire redux store will cost much perfomance whereas I only need to store one state slice.
Yes you can save only a slice such as:
saveState(store.getState().mySlice
This code is super simple and super useful. I used this in my project instead of using redux-persist because it gave me some errors. Thanks a lot
Hydration failed because the initial UI does not match what was rendered on the server.
did you find a solution for that ?