如何重置Redux store的状态?

635

我正在使用Redux来管理状态。

如何将存储库重置为其初始状态?

例如,假设我有两个用户账户(u1u2)。

想象以下事件序列:

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

  2. 用户u1注销。

  3. 用户u2登录应用程序而无需刷新浏览器。

此时,缓存的数据将与u1相关联,我希望清除它。

当第一个用户注销时,如何将Redux存储库重置为其初始状态?


12
从安全的角度考虑,最好在注销时清除状态。 - Clarkie
对于Redux Toolkit:如何在使用@reduxjs/toolkit的configureStore时重置Redux Store的状态? - Ajeet Shah
36个回答

1351

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

根减速器通常会将操作委托给由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时,无论是什么操作,它们都应该返回初始状态。让我们利用这个事实,在将累积的state传递给appReducer时有条件地剥离它:

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

  return appReducer(state, action)
}

现在,每当USER_LOGOUT触发时,所有的reducer都将重新初始化。如果需要,它们也可以返回与初始状态不同的内容,因为它们可以检查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);
};

21
丹,我很好奇,你的 reducer 也能做类似这样的事情吗?当接收到 CLEAR_DATA 动作时返回 initialState。代码如下:case 'CLEAR_DATA': return initialState - HussienK
6
@HussienK 这会起作用,但不适用于每个 reducer 的状态。 - Cory Danielson
12
以下是一种版本,其中您可以动态地组合reducer,以防使用异步reducer:`export const createRootReducer = asyncReducers => { const appReducer = combineReducers({ myReducer ...asyncReducers }); return (state, action) => { if (action.type === 'LOGOUT_USER') { state = undefined; } return appReducer(state, action); } };` - Ivo Sabev
4
这种方法是否完全清除了状态和它的所有历史记录?我是从安全角度考虑的:如果已经实施,一旦触发了“USER_LOGOUT”操作,是否仍然有可能从之前的状态数据中获取信息?(例如通过开发工具) - AlexKempton
4
这个解决方案对你们有效吗?在我的情况下,它清除了日志记录器上的状态,但是在用户界面上,我仍然能看到之前的组件(例如,如果一个表单已经预填充,在注销并重新登录后,我会看到输入字段中填写有之前的数据)。 - kaushikdr
显示剩余19条评论

97

Dan Abramov答案是正确的,但是当我们在使用react-router-redux包时,遇到了一个奇怪的问题。

我们的解决方法是不将状态设置为undefined,而是仍然使用当前路由缩减器。因此,如果您正在使用此软件包,请实施以下解决方案。

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

23
我认为这里的要点是,在注销时你可能不希望清除整个状态树 - 这种方法在任何子树的根减速器中同样适用,因此在只在你想要清除的子树的根减速器上应用此技术可能会更清晰,而不是在整个树的根减速器上挑选出“特殊”的子元素以清除它们。 - davnicwil
2
我觉得我正在经历你现在所提到的问题(在注销时它将设置正确路径的路由,但会加载完全不同的组件),我实施了类似于你的东西来修复它,但我认为immutable js 中的某些问题弄乱了它。最终,我创建了一个具有 RESET-STATE 动作的父级 reducer,并从该 reducer 继承,以避免完全接触路由。 - Neta Meta
遇到了类似的问题,这个解决了。谢谢。 - Lloyd Watkin
3
请注意,在 react-redux-router 中,属性应为 router 而不是 rounting - Mrchief
2
@Mrchief 这取决于您在 combineReducers() 中定义的内容..... 如果您有 combineReducers({routing: routingReducer}),那么它将如答案所述。 - Ben Lonsdale

78

定义一个操作:

const RESET_ACTION = {
  type: "RESET"
}

假设你在每个reducer中都使用了switchif-else来处理每个reducer中的多个操作。我接下来会以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 将始终保持默认状态。

store.dispatch(RESET_ACTION) 仅是详细说明。您很可能需要一个 action creator 来实现此目的。更好的方法是您拥有一个 LOGOUT_ACTION

一旦您调度了这个 LOGOUT_ACTION。一个自定义中间件可以使用 Redux-Saga 或 Redux-Thunk 拦截该操作。两种方式都可以调度另一个 'RESET' 操作。这样,store 的注销和重置将同步发生,您的 store 将准备好用于另一个用户登录。


3
我认为这比其他答案中将状态设置为undefined更好。当您的应用程序期望状态树但您提供了undefined时,与仅仅提供一个空树相比,会产生更多的错误和麻烦。 - worc
5
州实际上不会是未定义的,因为当reducers收到未定义状态时,它们会返回initialState。 - Guillaume
7
@worc认为,采用这种方法时,每次有人创建一个新的reducer,都需要记得添加reset case。 - Francute
1
我已经改变了我的想法,因为这两个原因,再加上 RESET_ACTION 是一个动作的概念。所以它本来就不应该出现在 reducer 中。 - worc
2
这绝对是正确的方法。将状态设置为任何除初始状态以外的值只会带来麻烦。 - Sebastian Serrano
显示剩余6条评论

32

以下是对Dan Abramov答案进行简化的回答:

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


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

这是最好的解决方案。 - Ashish Kumar
你把这段代码放在哪里? - Gilbert

30

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

10
这个解决方案对我有用,只需小改动。我需要为状态添加未定义的可选类型,以便它可以与 configureStore 一起使用。"state: ReturnType<typeof appReducer> | undefined" - Milko Lorinkov
Redux Toolkit的更完整答案在这里:https://dev59.com/hFIH5IYBdhLWcg3weuq6#73372455 - Gilbert
它救了我的一天。谢谢你。 - farzad

20

从安全的角度考虑,注销用户时最安全的做法是重置所有持久状态(例如cookies、localStorageIndexedDBWeb SQL等),并使用window.location.reload()进行硬刷新。有可能因为粗心或者恶意的开发人员在window、DOM等地方存储了一些敏感信息。清除所有持久状态并刷新浏览器是确保不泄露先前用户信息给下一个用户的唯一方法。

(当然,作为在共用计算机上的用户,您应该使用“隐私浏览”模式、自行关闭浏览器窗口、使用“清除浏览数据”功能等,但是作为开发人员,我们不能期望每个人都始终如此勤奋)


9
为什么有人给这个点了踩? 当你创建一个新的redux状态,内容为空时,之前的状态仍然保存在内存中,理论上可以访问它们的数据。 刷新浏览器是最安全的选择! - Wilhelm Sorban
谢谢。这就是我在寻找的内容,所有其他答案仅返回一个新状态,而不像OP所要求的重置它。 - Kalpesh Popat
这绝对是最好的答案。我花了一整天尝试了每个地方提到的所有方法,问题仍然不断地左右弹出。我一直在想是否应该重新加载整个东西,而不是浪费时间添加不必要的代码,而且我还是一个初学者。 - Kachinga

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

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

   return state
 }

你还可以触发一个操作,由所有或某些 reducer 处理,你希望将其重置为初始 store。一个操作可以触发重置整个状态,或者只是你认为适合的一部分状态。我认为这是最简单和最可控的方式。


13

如果使用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)
}

8

更新 NGRX4

如果您正在迁移到 NGRX 4,您可能已经从 迁移指南 中注意到,用于组合 reducer 的 rootreducer 方法已被替换为 ActionReducerMap 方法。起初,这种新的做法可能会让重置状态变得困难。实际上,这很简单,但是做事情的方式已经改变了。

这个解决方案受到 NGRX4 Github 文档中的元 reducers API 部分的启发。

首先,假设您正在使用 NGRX 的新选项 ActionReducerMap 来组合您的 reducers:

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


7

在使用 TypeScript 时,我采用了一种方法,基于 Dan Abramov答案(由于redux typings使得将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"})
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接