在 Redux 应用程序中写入 localStorage 的位置?
Where to write to localStorage in a Redux app?
我想将我的状态树的某些部分保存到 localStorage。这样做的合适地点是什么?减速器还是动作?
一句话:中间件
查看 redux-persist。或者自己写。
[2016 年 12 月 18 日更新] 编辑删除了两个类似项目的提及现在不活动或弃用。
Reducer 永远不是执行此操作的合适位置,因为 reducer 应该是纯净的并且没有副作用。
我建议只在订阅者中执行此操作:
store.subscribe(() => {
// persist your state
})
在创建商店之前,阅读那些持久化的部分:
const persistedState = // ...
const store = createStore(reducer, persistedState)
如果您使用 combineReducers()
,您会注意到未收到状态的 reducer 将使用其默认的 state
参数值正常“启动”。这可能非常方便。
建议您对订阅者进行去抖动处理,以免写入 localStorage 的速度过快,否则会出现性能问题。
最后,您可以创建一个封装它的中间件作为替代方案,但我会从订户开始,因为它是一个更简单的解决方案并且可以很好地完成工作。
要填写 Dan Abramov 的答案,您可以使用 store.subscribe()
,如下所示:
store.subscribe(()=>{
localStorage.setItem('reduxState', JSON.stringify(store.getState()))
})
在创建商店之前,检查 localStorage
并像这样解析您的密钥下的任何 JSON:
const persistedState = localStorage.getItem('reduxState')
? JSON.parse(localStorage.getItem('reduxState'))
: {}
然后将此 persistedState
常量传递给 createStore
方法,如下所示:
const store = createStore(
reducer,
persistedState,
/* any middleware... */
)
如果有人对上述解决方案有任何疑问,您可以自己写信给。让我告诉你我做了什么。忽略saga middleware
的东西只关注两个东西localStorageMiddleware
和reHydrateStore
的方法。 localStorageMiddleware
拉取所有 redux state
并将其放入 local storage
并且 rehydrateStore
将所有 applicationState
拉入本地存储(如果存在)并将其放入 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;
我无法回答@Gardezi,但基于他的代码的选项可能是:
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
)
);
不同之处在于我们只是保存了一些动作,您甚至可以使用去抖动功能来仅保存状态的最后一次交互
我来晚了一点,但我根据此处所述的示例实现了持久状态。如果您只想每 X 秒更新一次状态,这种方法可能对您有所帮助:
定义一个包装函数
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!")
}
}
在您的订阅者中调用包装函数
store.subscribe((state) =>
{
updateLocalStorage(store.getState())
});
在此示例中,状态每 5 秒最多 更新一次,无论触发更新的频率如何。
基于其他答案(和 Jam Creencia's Medium article)中提供的优秀建议和简短代码摘录,这里有一个完整的解决方案!
我们需要一个包含 2 个函数的文件,save/load 状态 to/from 本地存储:
// 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;
}
};
这些函数由 store.js 导入,我们在其中配置我们的商店:
注意:您需要添加一个依赖项: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';
// 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(
throttle( () => saveState(store.getState()), 1000)
);
export default store;
store 被导入到 index.js 中,因此它可以传递到包装 App.js 的 Provider 中:
// 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')
)
请注意,绝对导入需要将此更改为 YourProjectFolder/jsconfig。json - 这会告诉它在找不到文件时去哪里寻找文件首先。否则,您会看到有关尝试从 src.
外部导入内容的投诉
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
如果您不需要将所有 redux 存储复制到 localStorage,您可以使用特定的存储参数:
store.subscribe(()=>{
window.localStorage.setItem('currency', store.getState().lang)
})
并设置初始状态参数值,如:
const initialState = {
currency: window.localStorage.getItem('lang') ?? 'en',
}
在这种情况下,您不需要将 const persistedState
传递给 const store = createStore()
我一直在寻找关于如何使用 redux-toolkit-persist 将状态持久化到本地存储的完整示例,但没有成功,直到我遇到上面的 @canProm 响应来解决我的问题。
这就是对我有用的
//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'));
我想将我的状态树的某些部分保存到 localStorage。这样做的合适地点是什么?减速器还是动作?
一句话:中间件
查看 redux-persist。或者自己写。
[2016 年 12 月 18 日更新] 编辑删除了两个类似项目的提及现在不活动或弃用。
Reducer 永远不是执行此操作的合适位置,因为 reducer 应该是纯净的并且没有副作用。
我建议只在订阅者中执行此操作:
store.subscribe(() => {
// persist your state
})
在创建商店之前,阅读那些持久化的部分:
const persistedState = // ...
const store = createStore(reducer, persistedState)
如果您使用 combineReducers()
,您会注意到未收到状态的 reducer 将使用其默认的 state
参数值正常“启动”。这可能非常方便。
建议您对订阅者进行去抖动处理,以免写入 localStorage 的速度过快,否则会出现性能问题。
最后,您可以创建一个封装它的中间件作为替代方案,但我会从订户开始,因为它是一个更简单的解决方案并且可以很好地完成工作。
要填写 Dan Abramov 的答案,您可以使用 store.subscribe()
,如下所示:
store.subscribe(()=>{
localStorage.setItem('reduxState', JSON.stringify(store.getState()))
})
在创建商店之前,检查 localStorage
并像这样解析您的密钥下的任何 JSON:
const persistedState = localStorage.getItem('reduxState')
? JSON.parse(localStorage.getItem('reduxState'))
: {}
然后将此 persistedState
常量传递给 createStore
方法,如下所示:
const store = createStore(
reducer,
persistedState,
/* any middleware... */
)
如果有人对上述解决方案有任何疑问,您可以自己写信给。让我告诉你我做了什么。忽略saga middleware
的东西只关注两个东西localStorageMiddleware
和reHydrateStore
的方法。 localStorageMiddleware
拉取所有 redux state
并将其放入 local storage
并且 rehydrateStore
将所有 applicationState
拉入本地存储(如果存在)并将其放入 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;
我无法回答@Gardezi,但基于他的代码的选项可能是:
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
)
);
不同之处在于我们只是保存了一些动作,您甚至可以使用去抖动功能来仅保存状态的最后一次交互
我来晚了一点,但我根据此处所述的示例实现了持久状态。如果您只想每 X 秒更新一次状态,这种方法可能对您有所帮助:
定义一个包装函数
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!") } }
在您的订阅者中调用包装函数
store.subscribe((state) => { updateLocalStorage(store.getState()) });
在此示例中,状态每 5 秒最多 更新一次,无论触发更新的频率如何。
基于其他答案(和 Jam Creencia's Medium article)中提供的优秀建议和简短代码摘录,这里有一个完整的解决方案!
我们需要一个包含 2 个函数的文件,save/load 状态 to/from 本地存储:
// 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;
}
};
这些函数由 store.js 导入,我们在其中配置我们的商店:
注意:您需要添加一个依赖项: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';
// 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(
throttle( () => saveState(store.getState()), 1000)
);
export default store;
store 被导入到 index.js 中,因此它可以传递到包装 App.js 的 Provider 中:
// 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')
)
请注意,绝对导入需要将此更改为 YourProjectFolder/jsconfig。json - 这会告诉它在找不到文件时去哪里寻找文件首先。否则,您会看到有关尝试从 src.
外部导入内容的投诉{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
如果您不需要将所有 redux 存储复制到 localStorage,您可以使用特定的存储参数:
store.subscribe(()=>{
window.localStorage.setItem('currency', store.getState().lang)
})
并设置初始状态参数值,如:
const initialState = {
currency: window.localStorage.getItem('lang') ?? 'en',
}
在这种情况下,您不需要将 const persistedState
传递给 const store = createStore()
我一直在寻找关于如何使用 redux-toolkit-persist 将状态持久化到本地存储的完整示例,但没有成功,直到我遇到上面的 @canProm 响应来解决我的问题。 这就是对我有用的
//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'));