如何重置 Redux 存储的状态?

How to reset the state of a Redux store?

我正在使用 Redux 进行状态管理。
如何将商店重置为初始状态?

例如,假设我有两个用户帐户(u1u2)。
想象一下以下事件序列:

  1. 用户 u1 登录应用程序并执行某些操作,因此我们在商店中缓存了一些数据。

  2. 用户 u1 注销。

  3. 用户 u2 在不刷新浏览器的情况下登录应用程序。

此时缓存数据会关联u1,我想清理一下

如何在第一个用户退出时将 Redux 存储重置为初始状态?

一种方法是在您的应用程序中编写根减速器。

root reducer 通常会将处理操作委托给 combineReducers() 生成的 reducer。但是,每当它收到 USER_LOGOUT 操作时,它都会 return 重新回到初始状态。

例如,如果您的根减速器如下所示:

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 教给 return 响应 USER_LOGOUT 动作的初始状态。正如我们所知,当以 undefined 作为第一个参数调用时,reducer 应该 return 初始状态,无论操作如何。当我们将累积的 state 传递给 appReducer:

时,让我们使用这个事实有条件地去除它
 const rootReducer = (state, action) => {
  if (action.type === 'USER_LOGOUT') {
    return appReducer(undefined, action)
  }

  return appReducer(state, action)
}

现在,每当 USER_LOGOUT 触发时,所有的 reducer 都会重新初始化。如果他们愿意,他们还可以 return 一些与最初不同的东西,因为他们也可以检查 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 在存储引擎中保存状态副本,刷新时将从那里加载状态副本。

首先,您需要导入合适的storage engine,然后在将其设置为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);
};

只需注销 link 清除会话并刷新页面即可。您的商店不需要额外的代码。任何时候您想要完全重置状态,页面刷新都是一种简单且易于重复的处理方式。

我已经创建了一个组件来赋予 Redux 重置状态的能力,您只需要使用这个组件来增强您的商店并调度特定的 action.type 来触发重置。实现思路同Dan Abramov said in their .

Github: https://github.com/wwayne/redux-reset

Dan Abramov's 是正确的,除了我们在使用 react-router-redux 包和这种方法时遇到了一个奇怪的问题。

我们的解决方法是不将状态设置为 undefined,而是仍然使用当前的路由缩减器。所以如果你使用这个包,我建议实施下面的解决方案

const rootReducer = (state, action) => {
  if (action.type === 'USER_LOGOUT') {
    const { routing } = state
    state = { routing } 
  }
  return appReducer(state, action)
}

这种方法非常正确:破坏任何特定状态"NAME"以忽略并保留其他状态。

const rootReducer = (state, action) => {
    if (action.type === 'USER_LOGOUT') {
        state.NAME = undefined
    }
    return appReducer(state, action)
}

使用 Redux 如果应用了以下解决方案,假设我已经在我的所有 reducer 中设置了 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)
}

结合 Dan Abramov's , Ryan Irilli's and Rob Moorman's ,为了保持 router 状态并初始化状态树中的所有其他内容,我最终得到了这个:

const rootReducer = (state, action) => appReducer(action.type === LOGOUT ? {
    ...appReducer({}, {}),
    router: state && state.router || {}
  } : state, action);

我已经创建了清除状态的操作。因此,当我发送注销操作创建者时,我也会发送操作以清除状态。

用户记录操作

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
  }
};

我不确定这是否是最优的?

 const reducer = (state = initialState, { type, payload }) => {

   switch (type) {
      case RESET_STORE: {
        state = initialState
      }
        break
   }

   return state
 }

您还可以触发一个由所有或部分 reducer 处理的操作,您希望将其重置为初始存储。一个动作可以触发对你整个状态的重置,或者只是其中一个看起来适合你的部分。我相信这是最简单和最可控的方法。

你为什么不直接使用 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;
}

注意: 确保将操作默认值设置为 {} 并且没问题,因为您不想在检查 [=13= 时遇到错误] 在 switch 语句中。

另一种选择是:

store.dispatch({type: '@@redux/INIT'})

'@@redux/INIT' 是当你 createStore 时 redux 自动调度的动作类型,所以假设你的 reducer 都已经有一个默认值,这会被那些捕获并重新开始你的状态。不过,它可能被认为是 redux 的私有实现细节,所以买家要当心...

更新 NGRX4

如果您正在迁移到 NGRX 4,您可能已经从 migration guide 中注意到,用于组合 reducer 的 rootreducer 方法已被 ActionReducerMap 方法取代。起初,这种新的做事方式可能会使重置状态成为一项挑战。其实很简单,但是方法变了。

此解决方案的灵感来自 NGRX4 Github docs.

的 meta-reducers 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 实现相同效果的一种方法。

定义一个动作:

const RESET_ACTION = {
  type: "RESET"
}

然后在每个减速器中假设您使用 switchif-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 操作,reducer 都会使用默认状态更新存储。

现在,对于注销,您可以按如下方式处理:

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 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-actions,这里有一个使用 HOF(高阶函数)的快速解决方法 handleActions

import { handleActions } from 'redux-actions';

export function handleActionsEx(reducer, initialState) {
  const enhancedReducer = {
    ...reducer,
    RESET: () => initialState
  };
  return handleActions(enhancedReducer, initialState);
}

然后使用handleActionsEx代替原来的handleActions来处理减速器。

对这个问题给出了很好的想法,但对我来说效果不佳,因为我使用的是 redux-persist.
当与 redux-persist 一起使用时,简单地传递 undefined 状态不会触发持续行为,所以我知道我必须手动从存储中删除项目(在我的例子中是 React Native,因此 AsyncStorage)。

await AsyncStorage.removeItem('persist:root');

await persistor.flush(); // or await persistor.purge();

对我也不起作用——他们只是对我大喊大叫。 (例如,抱怨 "Unexpected key _persist ..."

然后我突然想到我想要的只是让每个单独的reducer return 在遇到RESET 动作类型时有自己的初始状态。这样,持久化就会自然地处理。显然没有上面的实用函数(handleActionsEx),我的代码看起来不会干(虽然它只是一个衬里,即 RESET: () => initialState),但我受不了,因为我喜欢元编程。

以下解决方案对我有用。

我将重置状态功能添加到 meta reducers.The 关键是要使用

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 {}

从安全角度来看,注销用户时最安全的做法是重置所有持久状态(e.x。cookies,localStorageIndexedDBWeb SQL, 等)并使用 window.location.reload() 硬刷新页面。草率的开发人员可能无意或有意地将一些敏感数据存储在 window、DOM 等中。清除所有持久状态并刷新浏览器是保证没有来自先前用户的信息的唯一方法泄露给下一个用户。

(当然,作为共享计算机上的用户,您应该使用 "private browsing" 模式,自己关闭浏览器 window,使用 "clear browsing data" 功能等,但作为开发者我们不能指望每个人都那么勤奋)

我发现 Dan Abramov's worked well for me, but it triggered the ESLint no-param-reassign error - 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)
}

我在使用 typescript 时的解决方法,建立在 Dan Abramov's 之上(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's 之外,我们不应该在 state = undefined 旁边明确地将操作设置为 action = {type: '@@INIT'}。对于上述动作类型,每个减速器 returns 初始状态。

在服务器中,我有一个变量:global.isSsr = true 在每个减速器中,我都有一个 const: initialState 要重置 Store 中的数据,我对每个 Reducer 执行以下操作:

示例appReducer.js

 const initialState = {
    auth: {},
    theme: {},
    sidebar: {},
    lsFanpage: {},
    lsChatApp: {},
    appSelected: {},
};

export default function (state = initialState, action) {
    if (typeof isSsr!=="undefined" && isSsr) { //<== using global.isSsr = true
        state = {...initialState};//<= important "will reset the data every time there is a request from the client to the server"
    }
    switch (action.type) {
        //...other code case here
        default: {
            return state;
        }
    }
}

最后在服务器的路由器上:

router.get('*', (req, res) => {
        store.dispatch({type:'reset-all-blabla'});//<= unlike any action.type // i use Math.random()
        // code ....render ssr here
});

只是对 Dan Abramov's 的简单回答:

const rootReducer = combineReducers({
    auth: authReducer,
    ...formReducers,
    routing
});


export default (state, action) =>
  rootReducer(action.type === 'USER_LOGOUT' ? undefined : state, action);

首先在我们的应用程序 initiation reducer 状态是 freshnew 默认 InitialState.

我们必须添加一个调用 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);
};

On Logout calling action for resetting state

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
    });
  }
}

Dan Abramov's 帮我破案了。然而,我遇到了一个情况,不是整个状态都必须被清除。所以我这样做了:

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;

一个对我有用的快速简单的选择是使用 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)

在您的注销函数中调度您的'reset'

store.dispatch({
    type: 'RESET'
})

为了将状态重置为初始状态,我编写了以下代码:

const appReducers = (state, action) =>
   combineReducers({ reducer1, reducer2, user })(
     action.type === "LOGOUT" ? undefined : state,
     action
);

我的做法是防止 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;
  }
};

有一件事Dan Abramov's 没有做,就是清除参数化选择器的缓存。如果您有这样的选择器:

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 docs

onLogout() {
  this.props.history.push('/login'); // send user to login page
  window.location.reload(); // refresh the page
}

对我来说最好的方法是设置 initialState 而不是 state:

  const reducer = createReducer(initialState,
  on(proofActions.cleanAdditionalInsuredState, (state, action) => ({
    ...initialState
  })),

如果要重置单个reducer

例如

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())

使用 Redux 工具包 and/or 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);
};

您可以通过将此代码添加到操作文件来清空 reducer 的数据,

首先导入所有类型:

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;

使用 Redux 工具包的方法:


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);
};