How to reset the state of a Redux store?

别说谁变了你拦得住时间么 提交于 2019-11-26 00:17:14

问题


I am using Redux for state management.
How do I reset the store to its initial state?

For example, let’s say I have two user accounts (u1 and u2).
Imagine the following sequence of events:

  1. User u1 logs into the app and does something, so we cache some data in the store.

  2. User u1 logs out.

  3. User u2 logs into the app without refreshing the browser.

At this point, the cached data will be associated with u1, and I would like to clean it up.

How can I reset the Redux store to its initial state when the first user logs out?


回答1:


One way to do that would be to write a root reducer in your application.

The root reducer would normally delegate handling the action to the reducer generated by combineReducers(). However, whenever it receives USER_LOGOUT action, it returns the initial state all over again.

For example, if your root reducer looked like this:

const rootReducer = combineReducers({
  /* your app’s top-level reducers */
})

You can rename it to appReducer and write a new rootReducer delegating to it:

const appReducer = combineReducers({
  /* your app’s top-level reducers */
})

const rootReducer = (state, action) => {
  return appReducer(state, action)
}

Now we just need to teach the new rootReducer to return the initial state after USER_LOGOUT action. As we know, reducers are supposed to return the initial state when they are called with undefined as the first argument, no matter the action. Let’s use this fact to conditionally strip the accumulated state as we pass it to appReducer:

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

  return appReducer(state, action)
}

Now, whenever USER_LOGOUT fires, all reducers will be initialized anew. They can also return something different than they did initially if they want to because they can check action.type as well.

To reiterate, the full new code looks like this:

const appReducer = combineReducers({
  /* your app’s top-level reducers */
})

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

  return appReducer(state, action)
}

Note that I’m not mutating the state here, I am merely reassigning the reference of a local variable called state before passing it to another function. Mutating a state object would be a violation of Redux principles.

In case you are using redux-persist, you may also need to clean your storage. Redux-persist keeps a copy of your state in a storage engine, and the state copy will be loaded from there on refresh.

First, you need to import the appropriate storage engine and then, to parse the state before setting it to undefined and clean each storage state key.

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

        state = undefined;
    }
    return appReducer(state, action);
};



回答2:


I'd like to point out that the accepted comment by Dan Abramov is correct except we experienced a strange issue when using the react-router-redux package along with this approach. Our fix was to not set state to undefined but rather still use the current routing reducer. So I would suggest implementing the solution below if you are using this package

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



回答3:


Define an action:

const RESET_ACTION = {
  type: "RESET"
}

Then in each of your reducers assuming you are using switch or if-else for handling multiple actions through each reducer. I am going to take the case for a 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; 
  }
}

This way whenever you call RESET action, you reducer will update the store with default state.

Now, for logout you can handle the like below:

const logoutHandler = () => {
    store.dispatch(RESET_ACTION)
    // Also the custom logic like for the rest of the logout handler
}

Every time a userlogs in, without a browser refresh. Store will always be at default.

store.dispatch(RESET_ACTION) just elaborates the idea. You will most likely have an action creator for the purpose. A much better way will be that you have a LOGOUT_ACTION.

Once you dispatch this LOGOUT_ACTION. A custom middleware can then intercept this action, either with Redux-Saga or Redux-Thunk. Both ways however, you can dispatch another action 'RESET'. This way store logout and reset will happen synchronously and your store will ready for another user login.




回答4:


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

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

   return state
 }

You can also fire an action which is handled by all or some reducers, that you want to reset to initial store. One action can trigger a reset to your whole state, or just a piece of it that seems fit to you. I believe this is the simplest and most controllable way of doing this.




回答5:


With Redux if have applied the following solution, which assumes I have set an initialState in all my reducers (e.g. { user: { name, email }}). In many components I check on these nested properties, so with this fix I prevent my renders methods are broken on coupled property conditions (e.g. if state.user.email, which will throw an error user is undefined if upper mentioned solutions).

const appReducer = combineReducers({
  tabs,
  user
})

const initialState = appReducer({}, {})

const rootReducer = (state, action) => {
  if (action.type === 'LOG_OUT') {
    state = initialState
  }

  return appReducer(state, action)
}



回答6:


Just a simplified answer to the best answer:

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


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



回答7:


UPDATE NGRX4

If you are migrating to NGRX 4, you may have noticed from the migration guide that the rootreducer method for combining your reducers has been replaced with ActionReducerMap method. At first, this new way of doing things might make resetting state a challenge. It is actually straight-forward, yet the way of doing this has changed.

This solution is inspired by the meta-reducers API section of the NGRX4 Github docs.

First, lets say your are combining your reducers like this using NGRX's new ActionReducerMap option:

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

Now, lets say you want to reset state from within 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 { }

`

And that is basically one way to achieve the same affect with NGRX 4.




回答8:


I've created a component to give Redux the ability of resetting state, you just need to use this component to enhance your store and dispatch a specific action.type to trigger reset. The thought of implementation is same as what @Dan Abramov said.

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




回答9:


Combining the approaches of Dan, Ryan and Rob, to account for keeping the router state and initializing everything else in the state tree, I ended up with this:

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



回答10:


I have created actions to clear state. So when I dispatch a logout action creator I dispatch actions to clear state as well.

User record action

export const clearUserRecord = () => ({
  type: CLEAR_USER_RECORD
});

Logout action creator

export const logoutUser = () => {
  return dispatch => {
    dispatch(requestLogout())
    dispatch(receiveLogout())
    localStorage.removeItem('auth_token')
    dispatch({ type: 'CLEAR_USER_RECORD' })
  }
};

Reducer

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

I am not sure if this is optimal?




回答11:


If you are using redux-actions, here's a quick workaround using a HOF(Higher Order Function) for handleActions.

import { handleActions } from 'redux-actions';

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

And then use handleActionsEx instead of original handleActions to handle reducers.

Dan's answer gives a great idea about this problem, but it didn't work out well for me, because I'm using redux-persist.
When used with redux-persist, simply passing undefined state didn't trigger persisting behavior, so I knew I had to manually remove item from storage (React Native in my case, thus AsyncStorage).

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

or

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

didn't work for me either - they just yelled at me. (e.g., complaining like "Unexpected key _persist ...")

Then I suddenly pondered all I want is just make every individual reducer return their own initial state when RESET action type is encountered. That way, persisting is handled naturally. Obviously without above utility function (handleActionsEx), my code won't look DRY (although it's just a one liner, i.e. RESET: () => initialState), but I couldn't stand it 'cuz I love metaprogramming.




回答12:


The accepted answer helped me solve my case. However, I encountered case where not-the-whole-state had to be cleared. So - I did it this way:

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;

Hope this helps someone else :)




回答13:


This approach is very right: Destruct any specific state "NAME" to ignore and keep others.

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



回答14:


Just an extension to @dan-abramov answer, sometimes we may need to retain certain keys from being reset.

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



回答15:


The following solution worked for me.

I added resetting state function to meta reducers.The key was to use

return reducer(undefined, action);

to set all reducers to initial state. Returning undefined instead was causing errors due to the fact that the structure of the store has been destroyed.

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



回答16:


From a security perspective, the safest thing to do when logging a user out is to reset all persistent state (e.x. cookies, localStorage, IndexedDB, Web SQL, etc) and do a hard refresh of the page using window.location.reload(). It's possible a sloppy developer accidentally or intentionally stored some sensitive data on window, in the DOM, etc. Blowing away all persistent state and refreshing the browser is the only way to guarantee no information from the previous user is leaked to the next user.

(Of course, as a user on a shared computer you should use "private browsing" mode, close the browser window yourself, use the "clear browsing data" function, etc, but as a developer we can't expect everyone to always be that diligent)




回答17:


A quick and easy option which worked for me was using redux-reset . Which was straightforward and also has some advanced options, for larger apps.

Setup in create store

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)

Dispatch your 'reset' in your logout function

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

Hope this helps




回答18:


My take to keep Redux from referencing to the same variable of the initial state:

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



回答19:


why don't you just use 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;
}

Note: make sure you set action default value to {} and you are ok because you don't want to encounter error when you check action.type inside the switch statement.




回答20:


I found that the accepted answer worked well for me, but it triggered the ESLint no-param-reassign error - https://eslint.org/docs/rules/no-param-reassign

Here's how I handled it instead, making sure to create a copy of the state (which is, in my understanding, the Reduxy thing to do...):

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

But maybe creating a copy of the state to just pass it into another reducer function that creates a copy of that is a little over-complicated? This doesn't read as nicely, but is more to-the-point:

export default (state, action) => {
    return appReducer(action.type === "LOGOUT" ? undefined : state, action)
}



回答21:


My workaround when working with typescript, built on top of Dan's answer (redux typings make it impossible to pass undefined to reducer as first argument, so I cache initial root state in a constant):

// 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"})
  }
}



回答22:


In addition to Dan Abramov's answer, shouldn't we explicitly set action as action = {type: '@@INIT'} alongside state = undefined. With above action type, every reducer returns the initial state.




回答23:


in server, i have a variable is: global.isSsr = true and in each reducer, i have a const is : initialState To reset the data in the Store, I do the following with each Reducer: example with 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;
        }
    }
}

finally on the server's router:

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



回答24:


The following solution works for me.

First on initiation of our application the reducer state is fresh and new with default InitialState.

We have to add an action that calls on APP inital load to persists default state.

While logging out of the application we can simple reAssign the default state and reducer will work just as new.

Main APP Container

  componentDidMount() {   
    this.props.persistReducerState();
  }

Main APP Reducer

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

Hope this solves your problem!




回答25:


For me to reset the state to its initial state, I wrote the following code:

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



回答26:


One thing the solution in the accepted answer doesn't do is clear the cache for parameterized selectors. If you have a selector like this:

export const selectCounter1 = (state: State) => state.counter1;
export const selectCounter2 = (state: State) => state.counter2;
export const selectTotal = createSelector(
  selectCounter1,
  selectCounter2,
  (counter1, counter2) => counter1 + counter2
);

Then you would have to release them on logout like this:

selectTotal.release();

Otherwise the memoized value for the last call of the selector and the values of the last parameters will still be in memory.

Code samples are from the ngrx docs.




回答27:


Another option is to:

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

'@@redux/INIT' is the action type that redux dispatches automatically when you createStore, so assuming your reducers all have a default already, this would get caught by those and start your state off fresh. It might be considered a private implementation detail of redux, though, so buyer beware...




回答28:


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



回答29:


Simply have your logout link clear session and refresh the page. No additional code needed for your store. Any time you want to completely reset the state a page refresh is a simple and easily repeatable way to handle it.



来源:https://stackoverflow.com/questions/35622588/how-to-reset-the-state-of-a-redux-store

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!