Reputation: 5458
I want to persist some parts of my state tree to the localStorage. What is the appropriate place to do so? Reducer or action?
Upvotes: 220
Views: 150106
Reputation: 373
Building on the excellent suggestions and short code excerpts provided in other answers (and Jam Creencia's Medium article), here's a complete solution!
We need a file containing 2 functions that save/load the state to/from local storage:
// FILE: src/common/localStorage/localStorage.js
// Pass in Redux store's state to save it to the user's browser local storage
export const saveState = (state) =>
{
try
{
const serializedState = JSON.stringify(state);
localStorage.setItem('state', serializedState);
}
catch
{
// We'll just ignore write errors
}
};
// Loads the state and returns an object that can be provided as the
// preloadedState parameter of store.js's call to configureStore
export const loadState = () =>
{
try
{
const serializedState = localStorage.getItem('state');
if (serializedState === null)
{
return undefined;
}
return JSON.parse(serializedState);
}
catch (error)
{
return undefined;
}
};
Those functions are imported by store.js where we configure our store:
NOTE: You'll need to add one dependency: npm install lodash.throttle
// FILE: src/app/redux/store.js
import { configureStore, applyMiddleware } from '@reduxjs/toolkit'
import throttle from 'lodash.throttle';
import rootReducer from "./rootReducer";
import middleware from './middleware';
import { saveState, loadState } from 'common/localStorage/localStorage';
const throttledSaveState = throttle(() => saveState(store.getState()), 1000);
// By providing a preloaded state (loaded from local storage), we can persist
// the state across the user's visits to the web app.
//
// READ: https://redux.js.org/recipes/configuring-your-store
const store = configureStore({
reducer: rootReducer,
middleware: middleware,
enhancer: applyMiddleware(...middleware),
preloadedState: loadState()
})
// We'll subscribe to state changes, saving the store's state to the browser's
// local storage. We'll throttle this to prevent excessive work.
store.subscribe(throttledSaveState);
export default store;
Note on Debounce vs Throttle:
- Debounce may be a better choice if you wish to wait until the user has finished making a series of rapid changes before saving.
- Throttle is good if you want to save the first change immediately instead of waiting, but this may have performance implications.
The store is imported into index.js so it can be passed into the Provider that wraps App.js:
// FILE: src/index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './app/core/App'
import store from './app/redux/store';
// Provider makes the Redux store available to any nested components
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Note that absolute imports require this change to YourProjectFolder/jsconfig.json - this tells it where to look for files if it can't find them at first. Otherwise, you'll see complaints about attempting to import something from outside of src.
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
Upvotes: 6
Reputation: 4062
I was looking badly for an entire example on how to persist state into a local storage using redux-toolkit-persist with no success until I came across @canProm response above to solve my issue. This is what is working for me
//package.json
"reduxjs-toolkit-persist": "^7.0.1",
"lodash": "^4.17.21"
//localstorage.ts
import localStorage from 'reduxjs-toolkit-persist/es/storage';
export const saveState = (state: any) => {
try {
console.log(state);
const serializableState = JSON.stringify(state);
localStorage.setItem('globalState', serializableState);
} catch (err) {
console.log('Redux was not able to persist the state into the localstorage');
}
};
export const loadState = () => {
try {
const serializableState: string | any =
localStorage.getItem('globalState');
return serializableState !== null || serializableState === undefined ? JSON.parse(serializableState) : undefined;
} catch (error) {
return undefined;
}
};
//slices - actions
//reduxjs-toolkit-slices.ts
import { combineReducers, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { UserModel } from '../model/usermodel';
import { GlobalState } from './type';
const deaultState: GlobalState = {
counter: 0,
isLoggedIn: false
};
const stateSlice = createSlice({
name: "state",
initialState: deaultState,
reducers: {
isLoggedIn: (state, action: PayloadAction<boolean>) => {
console.log('isLogged');
console.log(state.isLoggedIn);
console.log(action);
state.isLoggedIn = action.payload;
console.log(state.isLoggedIn);
},
setUserDetails: (state, action: PayloadAction<UserModel>) => {
console.log('setUserDetails');
console.log(state);
console.log(action);
//state.userContext.user = action.payload;
}
}
});
//export actions under slices
export const {
isLoggedIn: isUserLoggedAction,
setUserDetails: setUserDetailActions
} = stateSlice.actions;
//TODO: use the optimal way for combining reducer using const
//combine reducer from all slice
export const combineReducer = combineReducers({
stateReducer: stateSlice.reducer
});
//storeConfig
//reduxjs-toolkit-store.ts
import { configureStore } from '@reduxjs/toolkit';
import { throttle } from 'lodash';
import { persistReducer } from 'reduxjs-toolkit-persist';
import autoMergeLevel2 from 'reduxjs-toolkit-persist/lib/stateReconciler/autoMergeLevel2';
import storage from 'reduxjs-toolkit-persist/lib/storage';
import { loadState, saveState } from './localStorage';
import { combineReducer } from './reduxjs-toolkit-slices';
// persist config
const persistConfig = {
key: 'root',
storage: storage,
stateReconciler: autoMergeLevel2,
};
const persistedReducer = persistReducer(persistConfig, combineReducer);
// export reducers under slices
const store = configureStore({
reducer: persistedReducer,
devTools: process.env.NODE_ENV !== 'production',
preloadedState: loadState(), //call loadstate method to initiate store from localstorage
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
thunk: true,
serializableCheck: false,
}),
});
// handle state update event. Whenever the state will change, this subscriber will call the saveState methode to update and persist the state into the store
store.subscribe(throttle(() => {
saveState(store.getState());
}, 1000));
export default store;
//App.ts
import { persistStore } from 'reduxjs-toolkit-persist';
import { PersistGate } from 'reduxjs-toolkit-persist/integration/react';
import './i18n';
let persistor = persistStore(store);
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={<div>Loading .....</div>} persistor={persistor}>
<HalocarburesRouter />
</PersistGate>
</Provider>,
document.getElementById('ingenierieMdn'));
Upvotes: 1
Reputation: 166
If you don't need to copy all redux store to localStorage you can use the specific store arguments:
store.subscribe(()=>{
window.localStorage.setItem('currency', store.getState().lang)
})
And set initial state argument value like:
const initialState = {
currency: window.localStorage.getItem('lang') ?? 'en',
}
In this case, you don't need to pass const persistedState
to const store = createStore()
Upvotes: 3
Reputation: 5432
To fill in the blanks of Dan Abramov's answer you could use store.subscribe()
like this:
store.subscribe(()=>{
localStorage.setItem('reduxState', JSON.stringify(store.getState()))
})
Before creating the store, check localStorage
and parse any JSON under your key like this:
const persistedState = localStorage.getItem('reduxState')
? JSON.parse(localStorage.getItem('reduxState'))
: {}
You then pass this persistedState
constant to your createStore
method like this:
const store = createStore(
reducer,
persistedState,
/* any middleware... */
)
Upvotes: 193
Reputation: 189
I'm a bit late but I implemented a persistent state according to the examples stated here. If you want to update the state only every X seconds, this approach may help you:
Define a wrapper function
let oldTimeStamp = (Date.now()).valueOf()
const millisecondsBetween = 5000 // Each X milliseconds
function updateLocalStorage(newState)
{
if(((Date.now()).valueOf() - oldTimeStamp) > millisecondsBetween)
{
saveStateToLocalStorage(newState)
oldTimeStamp = (Date.now()).valueOf()
console.log("Updated!")
}
}
Call a wrapper function in your subscriber
store.subscribe((state) =>
{
updateLocalStorage(store.getState())
});
In this example, the state is updated at most each 5 seconds, regardless how often an update is triggered.
Upvotes: 2
Reputation: 1256
I cannot answer @Gardezi but an option based on his code could be:
const rootReducer = combineReducers({
users: authReducer,
});
const localStorageMiddleware = ({ getState }) => {
return next => action => {
const result = next(action);
if ([ ACTIONS.LOGIN ].includes(result.type)) {
localStorage.setItem(appConstants.APP_STATE, JSON.stringify(getState()))
}
return result;
};
};
const reHydrateStore = () => {
const data = localStorage.getItem(appConstants.APP_STATE);
if (data) {
return JSON.parse(data);
}
return undefined;
};
return createStore(
rootReducer,
reHydrateStore(),
applyMiddleware(
thunk,
localStorageMiddleware
)
);
the difference is that we are just saving some actions, you could event use a debounce function to save only the last interaction of your state
Upvotes: 5
Reputation: 2842
If anybody is having any problem with the above solutions, you can write your own to. Let me show you what I did. Ignore saga middleware
things just focus on two things localStorageMiddleware
and reHydrateStore
method. the localStorageMiddleware
pull all the redux state
and puts it in local storage
and rehydrateStore
pull all the applicationState
in local storage if present and puts it in redux store
import {createStore, applyMiddleware} from 'redux'
import createSagaMiddleware from 'redux-saga';
import decoristReducers from '../reducers/decorist_reducer'
import sagas from '../sagas/sagas';
const sagaMiddleware = createSagaMiddleware();
/**
* Add all the state in local storage
* @param getState
* @returns {function(*): function(*=)}
*/
const localStorageMiddleware = ({getState}) => { // <--- FOCUS HERE
return (next) => (action) => {
const result = next(action);
localStorage.setItem('applicationState', JSON.stringify(
getState()
));
return result;
};
};
const reHydrateStore = () => { // <-- FOCUS HERE
if (localStorage.getItem('applicationState') !== null) {
return JSON.parse(localStorage.getItem('applicationState')) // re-hydrate the store
}
}
const store = createStore(
decoristReducers,
reHydrateStore(),// <-- FOCUS HERE
applyMiddleware(
sagaMiddleware,
localStorageMiddleware,// <-- FOCUS HERE
)
)
sagaMiddleware.run(sagas);
export default store;
Upvotes: 13
Reputation: 24825
In a word: middleware.
Check out redux-persist. Or write your own.
[UPDATE 18 Dec 2016] Edited to remove mention of two similar projects now inactive or deprecated.
Upvotes: 53
Reputation: 268313
Reducer is never an appropriate place to do this because reducers should be pure and have no side effects.
I would recommend just doing it in a subscriber:
store.subscribe(() => {
// persist your state
})
Before creating the store, read those persisted parts:
const persistedState = // ...
const store = createStore(reducer, persistedState)
If you use combineReducers()
you’ll notice that reducers that haven’t received the state will “boot up” as normal using their default state
argument value. This can be pretty handy.
It is advisable that you debounce your subscriber so you don’t write to localStorage too fast, or you’ll have performance problems.
Finally, you can create a middleware that encapsulates that as an alternative, but I’d start with a subscriber because it’s a simpler solution and does the job well.
Upvotes: 281