[英]How to reset the state of a Redux store?
我正在使用 Redux 进行状态管理。
如何将商店重置为初始状态?
例如,假设我有两个用户帐户( u1
和u2
)。
想象以下事件序列:
用户u1
登录应用程序并执行某些操作,因此我们在商店中缓存了一些数据。
用户u1
注销。
用户u2
无需刷新浏览器即可登录应用程序。
此时缓存的数据会关联到u1
,我想清理一下。
当第一个用户注销时,如何将 Redux 存储重置为其初始状态?
一种方法是在您的应用程序中编写一个根减速器。
根减速器通常会将处理操作委托给由combineReducers()
生成的减速器。 但是,每当它接收到USER_LOGOUT
操作时,它都会重新返回初始状态。
例如,如果您的根减速器如下所示:
const rootReducer = combineReducers({
/* your app’s top-level reducers */
})
您可以将其重命名为appReducer
并编写一个新的rootReducer
委托给它:
const appReducer = combineReducers({
/* your app’s top-level reducers */
})
const rootReducer = (state, action) => {
return appReducer(state, action)
}
现在我们只需要教新的rootReducer
返回初始状态以响应USER_LOGOUT
操作。 正如我们所知,无论操作如何,当使用undefined
作为第一个参数调用 reducer 时,它们都应该返回初始状态。 当我们将它传递给appReducer
让我们使用这个事实来有条件地剥离累积state
:
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
return appReducer(undefined, action)
}
return appReducer(state, action)
}
现在,每当USER_LOGOUT
触发时,所有减速器都将重新初始化。 如果他们愿意,他们也可以返回与最初不同的东西,因为他们也可以检查action.type
。
重申一下,完整的新代码如下所示:
const appReducer = combineReducers({
/* your app’s top-level reducers */
})
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
return appReducer(undefined, action)
}
return appReducer(state, action)
}
如果您使用的是redux-persist ,您可能还需要清理您的存储。 Redux-persist 在存储引擎中保留一份状态副本,状态副本将在刷新时从那里加载。
首先,您需要导入适当的存储引擎,然后在将其设置为undefined
之前解析状态并清理每个存储状态键。
const rootReducer = (state, action) => {
if (action.type === SIGNOUT_REQUEST) {
// for all keys defined in your persistConfig(s)
storage.removeItem('persist:root')
// storage.removeItem('persist:otherKey')
return appReducer(undefined, action);
}
return appReducer(state, action);
};
Dan Abramov的回答是正确的,只是我们在使用 react-router-redux 包和这种方法时遇到了一个奇怪的问题。
我们的修复是不将状态设置为undefined
,而是仍然使用当前的路由减少器。 因此,如果您正在使用此软件包,我建议您实施以下解决方案
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
const { routing } = state
state = { routing }
}
return appReducer(state, action)
}
定义一个动作:
const RESET_ACTION = {
type: "RESET"
}
然后在每个减速器中假设您使用switch
或if-else
通过每个减速器处理多个动作。 我要拿这个案子switch
。
const INITIAL_STATE = {
loggedIn: true
}
const randomReducer = (state=INITIAL_STATE, action) {
switch(action.type) {
case 'SOME_ACTION_TYPE':
//do something with it
case "RESET":
return INITIAL_STATE; //Always return the initial state
default:
return state;
}
}
这样,每当您调用RESET
操作时,您的减速器都会使用默认状态更新存储。
现在,对于注销,您可以处理如下:
const logoutHandler = () => {
store.dispatch(RESET_ACTION)
// Also the custom logic like for the rest of the logout handler
}
每次用户登录时,无需刷新浏览器。 商店将始终处于默认状态。
store.dispatch(RESET_ACTION)
只是阐述了这个想法。 为此,您很可能会有一个动作创建者。 更好的方法是你有一个LOGOUT_ACTION
。
一旦你发送了这个LOGOUT_ACTION
。 然后,自定义中间件可以使用 Redux-Saga 或 Redux-Thunk 拦截此操作。 但是,这两种方式都可以发送另一个动作“RESET”。 这样,商店注销和重置将同步发生,您的商店将为另一个用户登录做好准备。
只是对Dan Abramov的回答的简化回答:
const rootReducer = combineReducers({
auth: authReducer,
...formReducers,
routing
});
export default (state, action) =>
rootReducer(action.type === 'USER_LOGOUT' ? undefined : state, action);
const reducer = (state = initialState, { type, payload }) => {
switch (type) {
case RESET_STORE: {
state = initialState
}
break
}
return state
}
您还可以触发由所有或某些减速器处理的操作,您希望将其重置为初始存储。 一个动作可以触发整个状态的重置,或者只是其中一部分似乎适合你。 我相信这是最简单和最可控的方法。
从安全角度来看,注销用户时最安全的做法是重置所有持久状态(例如 cookie、 localStorage
、 IndexedDB
、 Web SQL
等)并使用window.location.reload()
对页面进行硬刷新. 粗心的开发者可能会无意或故意将一些敏感数据存储在window
、 DOM 等中。吹掉所有持久状态并刷新浏览器是保证前一个用户的信息不会泄露给下一个用户的唯一方法。
(当然,作为共享电脑的用户,你应该使用“隐私浏览”模式,自己关闭浏览器窗口,使用“清除浏览数据”功能等,但作为开发者我们不能期望每个人总是那个勤奋)
对于 Redux,如果应用了以下解决方案,假设我已经在所有减速器中设置了一个initialState
(例如{ user: { name, email }}
)。 在我检查这些嵌套属性的许多组件中,因此通过此修复程序,我可以防止我的渲染方法在耦合属性条件下被破坏(例如,如果state.user.email
,如果上面提到的解决方案将抛出错误user is undefined
)。
const appReducer = combineReducers({
tabs,
user
})
const initialState = appReducer({}, {})
const rootReducer = (state, action) => {
if (action.type === 'LOG_OUT') {
state = initialState
}
return appReducer(state, action)
}
使用Redux Toolkit和/或Typescript :
const appReducer = combineReducers({
/* your app’s top-level reducers */
});
const rootReducer = (
state: ReturnType<typeof appReducer>,
action: AnyAction
) => {
/* if you are using RTK, you can import your action and use it's type property instead of the literal definition of the action */
if (action.type === logout.type) {
return appReducer(undefined, { type: undefined });
}
return appReducer(state, action);
};
更新 NGRX4
如果您要迁移到 NGRX 4,您可能已经从迁移指南中注意到用于组合您的减速器的rootreducer
方法已被ActionReducerMap
方法取代。 起初,这种新的做事方式可能会使重置状态成为一项挑战。 这实际上很简单,但这样做的方式已经改变。
该解决方案的灵感来自NGRX4 Github 文档的元减速器 API 部分。
首先,假设您正在使用 NGRX 的新ActionReducerMap
选项组合这样的减速器:
//index.reducer.ts
export const reducers: ActionReducerMap<State> = {
auth: fromAuth.reducer,
layout: fromLayout.reducer,
users: fromUsers.reducer,
networks: fromNetworks.reducer,
routingDisplay: fromRoutingDisplay.reducer,
routing: fromRouting.reducer,
routes: fromRoutes.reducer,
routesFilter: fromRoutesFilter.reducer,
params: fromParams.reducer
}
现在,假设您想从app.module
重置状态
//app.module.ts
import { IndexReducer } from './index.reducer';
import { StoreModule, ActionReducer, MetaReducer } from '@ngrx/store';
...
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
return function(state, action) {
switch (action.type) {
case fromAuth.LOGOUT:
console.log("logout action");
state = undefined;
}
return reducer(state, action);
}
}
export const metaReducers: MetaReducer<any>[] = [debug];
@NgModule({
imports: [
...
StoreModule.forRoot(reducers, { metaReducers}),
...
]
})
export class AppModule { }
这基本上是使用 NGRX 4 实现相同效果的一种方法。
我在使用打字稿时的解决方法,建立在Dan Abramov的答案之上(redux 类型使得不可能将undefined
作为第一个参数传递给 reducer,所以我将初始根状态缓存在一个常量中):
// store
export const store: Store<IStoreState> = createStore(
rootReducer,
storeEnhacer,
)
export const initialRootState = {
...store.getState(),
}
// root reducer
const appReducer = combineReducers<IStoreState>(reducers)
export const rootReducer = (state: IStoreState, action: IAction<any>) => {
if (action.type === "USER_LOGOUT") {
return appReducer(initialRootState, action)
}
return appReducer(state, action)
}
// auth service
class Auth {
...
logout() {
store.dispatch({type: "USER_LOGOUT"})
}
}
结合Dan Abramov的回答、 Ryan Irilli的回答和Rob Moorman的回答,为了保持router
状态并初始化状态树中的所有其他内容,我最终得到了这个:
const rootReducer = (state, action) => appReducer(action.type === LOGOUT ? {
...appReducer({}, {}),
router: state && state.router || {}
} : state, action);
只需让您的注销链接清除会话并刷新页面即可。 您的商店不需要额外的代码。 任何时候您想要完全重置状态,页面刷新都是一种简单且易于重复的处理方式。
我创建了一个组件来让 Redux 能够重置状态,你只需要使用这个组件来增强你的商店并调度一个特定的action.type
来触发重置。 实施的想法与Dan Abramov在他们的回答中所说的相同。
Github: https : //github.com/wwayne/redux-reset
我已经创建了清除状态的操作。 因此,当我调度注销操作创建者时,我也会调度操作以清除状态。
用户记录操作
export const clearUserRecord = () => ({
type: CLEAR_USER_RECORD
});
注销操作创建者
export const logoutUser = () => {
return dispatch => {
dispatch(requestLogout())
dispatch(receiveLogout())
localStorage.removeItem('auth_token')
dispatch({ type: 'CLEAR_USER_RECORD' })
}
};
减速器
const userRecords = (state = {isFetching: false,
userRecord: [], message: ''}, action) => {
switch (action.type) {
case REQUEST_USER_RECORD:
return { ...state,
isFetching: true}
case RECEIVE_USER_RECORD:
return { ...state,
isFetching: false,
userRecord: action.user_record}
case USER_RECORD_ERROR:
return { ...state,
isFetching: false,
message: action.message}
case CLEAR_USER_RECORD:
return {...state,
isFetching: false,
message: '',
userRecord: []}
default:
return state
}
};
我不确定这是否最佳?
我要防止 Redux 引用初始状态的相同变量:
// write the default state as a function
const defaultOptionsState = () => ({
option1: '',
option2: 42,
});
const initialState = {
options: defaultOptionsState() // invoke it in your initial state
};
export default (state = initialState, action) => {
switch (action.type) {
case RESET_OPTIONS:
return {
...state,
options: defaultOptionsState() // invoke the default function to reset this part of the state
};
default:
return state;
}
};
如果您使用的是redux-actions ,这里有一个使用 HOF(高阶函数)作为handleActions
的快速解决方法。
import { handleActions } from 'redux-actions';
export function handleActionsEx(reducer, initialState) {
const enhancedReducer = {
...reducer,
RESET: () => initialState
};
return handleActions(enhancedReducer, initialState);
}
然后使用handleActionsEx
而不是原始的handleActions
来处理减速器。
Dan 的回答对这个问题给出了一个很好的主意,但对我来说效果不佳,因为我使用的是redux-persist
。
当与redux-persist
,简单地传递undefined
状态不会触发持久化行为,所以我知道我必须手动从存储中删除项目(在我的情况下是 React Native,因此是AsyncStorage
)。
await AsyncStorage.removeItem('persist:root');
或者
await persistor.flush(); // or await persistor.purge();
对我也不起作用——他们只是冲我大喊大叫。 (例如,像"Unexpected key _persist ..."这样的抱怨)
然后我突然想到我想要的只是让每个减速器在遇到RESET
操作类型时返回自己的初始状态。 这样,持久化就自然而然地处理了。 很明显,如果没有上面的实用函数( handleActionsEx
),我的代码看起来不会很干(虽然它只是一个单行代码,即RESET: () => initialState
),但我无法忍受,因为我喜欢元编程。
以下解决方案对我有用。
我在元减速器中添加了重置状态功能。关键是要使用
return reducer(undefined, action);
将所有减速器设置为初始状态。 由于存储的结构已被破坏,返回undefined
会导致错误。
/reducers/index.ts
export function resetState(reducer: ActionReducer<State>): ActionReducer<State> {
return function (state: State, action: Action): State {
switch (action.type) {
case AuthActionTypes.Logout: {
return reducer(undefined, action);
}
default: {
return reducer(state, action);
}
}
};
}
export const metaReducers: MetaReducer<State>[] = [ resetState ];
app.module.ts
import { StoreModule } from '@ngrx/store';
import { metaReducers, reducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers, { metaReducers })
]
})
export class AppModule {}
Dan Abramov的回答帮助我解决了我的案子。 但是,我遇到了不必清除整个状态的情况。 所以我是这样做的:
const combinedReducer = combineReducers({
// my reducers
});
const rootReducer = (state, action) => {
if (action.type === RESET_REDUX_STATE) {
// clear everything but keep the stuff we want to be preserved ..
delete state.something;
delete state.anotherThing;
}
return combinedReducer(state, action);
}
export default rootReducer;
只是@dan-abramov答案的扩展,有时我们可能需要保留某些键不被重置。
const retainKeys = ['appConfig'];
const rootReducer = (state, action) => {
if (action.type === 'LOGOUT_USER_SUCCESS' && state) {
state = !isEmpty(retainKeys) ? pick(state, retainKeys) : undefined;
}
return appReducer(state, action);
};
对我有用的快速简便的选项是使用redux-reset 。 对于较大的应用程序,这很简单,并且还有一些高级选项。
在创建商店中设置
import reduxReset from 'redux-reset'
// ...
const enHanceCreateStore = compose(
applyMiddleware(...),
reduxReset() // Will use 'RESET' as default action.type to trigger reset
)(createStore)
const store = enHanceCreateStore(reducers)
在您的注销功能中发送您的“重置”
store.dispatch({
type: 'RESET'
})
我发现Dan Abramov的回答对我很有效,但它触发了 ESLint no-param-reassign
错误 - https://eslint.org/docs/rules/no-param-reassign
这是我处理它的方式,确保创建状态的副本(在我看来,这是 Reduxy 要做的事情......):
import { combineReducers } from "redux"
import { routerReducer } from "react-router-redux"
import ws from "reducers/ws"
import session from "reducers/session"
import app from "reducers/app"
const appReducer = combineReducers({
"routing": routerReducer,
ws,
session,
app
})
export default (state, action) => {
const stateCopy = action.type === "LOGOUT" ? undefined : { ...state }
return appReducer(stateCopy, action)
}
但也许创建状态的副本以将其传递给另一个创建副本的 reducer 函数有点过于复杂? 这读起来不太好,但更切中要害:
export default (state, action) => {
return appReducer(action.type === "LOGOUT" ? undefined : state, action)
}
我正在使用Redux进行状态管理。
如何将商店重置为初始状态?
例如,假设我有两个用户帐户( u1
和u2
)。
想象以下事件序列:
用户u1
登录到该应用并执行某项操作,因此我们在存储中缓存了一些数据。
用户u1
注销。
用户u2
登录到应用程序,而无需刷新浏览器。
此时,缓存的数据将与u1
关联,我想对其进行清理。
当第一个用户注销时,如何将Redux存储重置为其初始状态?
我正在使用Redux进行状态管理。
如何将商店重置为初始状态?
例如,假设我有两个用户帐户( u1
和u2
)。
想象以下事件序列:
用户u1
登录到该应用并执行某项操作,因此我们在存储中缓存了一些数据。
用户u1
注销。
用户u2
登录到应用程序,而无需刷新浏览器。
此时,缓存的数据将与u1
关联,我想对其进行清理。
当第一个用户注销时,如何将Redux存储重置为其初始状态?
首先对我们的应用程序开始减速状态是耳目一新默认初始化状态。
我们必须添加一个调用 APP 初始加载的操作以保持默认状态。
在注销应用程序时,我们可以简单地重新分配默认状态,reducer 将像new 一样工作。
主APP容器
componentDidMount() {
this.props.persistReducerState();
}
主APP减速机
const appReducer = combineReducers({
user: userStatusReducer,
analysis: analysisReducer,
incentives: incentivesReducer
});
let defaultState = null;
export default (state, action) => {
switch (action.type) {
case appActions.ON_APP_LOAD:
defaultState = defaultState || state;
break;
case userLoginActions.USER_LOGOUT:
state = defaultState;
return state;
default:
break;
}
return appReducer(state, action);
};
在注销时调用用于重置状态的操作
function* logoutUser(action) {
try {
const response = yield call(UserLoginService.logout);
yield put(LoginActions.logoutSuccess());
} catch (error) {
toast.error(error.message, {
position: toast.POSITION.TOP_RIGHT
});
}
}
为了将状态重置为初始状态,我编写了以下代码:
const appReducers = (state, action) =>
combineReducers({ reducer1, reducer2, user })(
action.type === "LOGOUT" ? undefined : state,
action
);
Dan Abramov的回答没有做的一件事是清除参数化选择器的缓存。 如果您有这样的选择器:
export const selectCounter1 = (state: State) => state.counter1;
export const selectCounter2 = (state: State) => state.counter2;
export const selectTotal = createSelector(
selectCounter1,
selectCounter2,
(counter1, counter2) => counter1 + counter2
);
然后你必须像这样在注销时释放它们:
selectTotal.release();
否则,选择器最后一次调用的记忆值和最后一个参数的值仍将在内存中。
代码示例来自ngrx 文档。
onLogout() { this.props.history.push('/login'); // send user to login page window.location.reload(); // refresh the page }
使用 Redux Toolkit 的方法:
export const createRootReducer = (history: History) => {
const rootReducerFn = combineReducers({
auth: authReducer,
users: usersReducer,
...allOtherReducers,
router: connectRouter(history),
});
return (state: Parameters<typeof rootReducerFn>[0], action: Parameters<typeof rootReducerFn>[1]) =>
rootReducerFn(action.type === appActions.reset.type ? undefined : state, action);
};
这种方法非常正确:销毁任何特定状态“NAME”以忽略并保留其他状态。
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
state.NAME = undefined
}
return appReducer(state, action)
}
为什么不直接使用return module.exports.default()
;)
export default (state = {pending: false, error: null}, action = {}) => {
switch (action.type) {
case "RESET_POST":
return module.exports.default();
case "SEND_POST_PENDING":
return {...state, pending: true, error: null};
// ....
}
return state;
}
注意:确保您将 action 默认值设置为{}
并且您没问题,因为您不想在 switch 语句中检查action.type
时遇到错误。
另一种选择是:
store.dispatch({type: '@@redux/INIT'})
'@@redux/INIT'
是当你createStore
时 redux 自动调度的动作类型,所以假设你的 reducer 都已经有一个默认值,这会被那些捕获并重新启动你的状态。 不过,它可能被认为是 redux 的私有实现细节,所以买家要小心......
对我来说最有效的是设置initialState
而不是state
:
const reducer = createReducer(initialState,
on(proofActions.cleanAdditionalInsuredState, (state, action) => ({
...initialState
})),
如果要重置单个减速器
例如
const initialState = { isLogged: false } //this will be your action export const resetReducer = () => { return { type: "RESET" } } export default (state = initialState, { type, payload }) => { switch (type) { //your actions will come her case "RESET": return { ...initialState } } } //and from your frontend dispatch(resetReducer())
您可以通过将此代码添加到操作文件中来清空减速器的数据,
首先导入所有类型:
import * as types from './types';
将此代码添加到注销操作
for(let key of Object.values(types)) {
dispatch({ type: key, payload: [] });
}
npm install redux-reset
import reduxReset from 'redux-reset'
...
const enHanceCreateStore = compose(
applyMiddleware(...),
reduxReset() // Will use 'RESET' as default action.type to trigger reset
)(createStore)
const store = enHanceCreateStore(reducers)
https://github.com/wwayne/redux-reset
只需编辑声明减速器的文件
import { combineReducers } from 'redux';
import gets from '../';
const rootReducer = (state, action) => {
let asReset = action.type === 'RESET_STORE';
const reducers = combineReducers({
gets,
});
const transition = {
true() {
return reducers({}, action);
},
false() {
return reducers(state, action);
},
};
return transition[asReset] && transition[asReset]();
};
export default rootReducer;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.