27

I am using Angular 2 with ngrx/store. I want to reset the whole store states when user dispatch USER_LOGOUT.

I read the Dan Abramov's answer of How to reset the state of a Redux store?, but I didn't figure out how to write rootReducer correctly and where to put it when using ngrx/store.

Or is there any other way to handle this in ngrx/store?

bootstrap(App, [
    provideStore(
      compose(
        storeFreeze,
        storeLogger(),
        combineReducers
      )({
        router: routerReducer,
        foo: fooReducer,
        bar: barReducer
      })
    )
  ]);
Community
  • 1
  • 1
Hongbo Miao
  • 45,290
  • 60
  • 174
  • 267

5 Answers5

46

In ngrx/store 4.x, this can be accomplished with Meta-reducers. As I understand it, all actions are passing through the metareducers before being handed over to the feature reducers. This gives us the opportunity to change/reset the state first.

Here's an example.

This is my metareducer function: in case the action is of type LOGOUT, the state is re-initialized.

function logout(reducer) {
  return function (state, action) {
    return reducer(action.type === LOGOUT ? undefined : state, action);
  }
}

Below you see how the metareducer is configured along with the feature reducers. Should there be more than 1 metareducer, then they are evaluated from right to left

StoreModule.forRoot({rooms: roomReducer, user: userReducer}, {metaReducers: [logout]})

Finally, I also have an @effect where I navigate to the login page

@Effect({dispatch: false}) logout: Observable<Action> = 
this.actions$.ofType(LOGOUT)
  .do(() => {
    // ... some more stuff here ...
    this.router.navigate(['/login page'])
});
David Bulté
  • 2,988
  • 3
  • 31
  • 43
16

This answer is specific to ngrx version 2. The question has another, more recent answer that explains how the same can be done with ngrx version 4.


compose builds the ngrx root reducer.

The arguments passed to compose are functions that return a reducer - composed from the reducer they themselves are passed as an argument. You can compose the resetting of your store like this:

import { compose } from "@ngrx/core/compose";

...

bootstrap(App, [
  provideStore(
    compose(
      storeFreeze,
      storeLogger(),
      (reducer: Function) => {
        return function(state, action) {
          if (action.type === 'USER_LOGOUT') {
            state = undefined;
          }
          return reducer(state, action);
        };
      },
      combineReducers
    )({
      router: routerReducer,
      foo: fooReducer,
      bar: barReducer
    })
  )
]);

Note that this will reset all of the store's state - including the router. If that's not what you want, you could tweak the example.

With introduction of NgModule the bootstrapping has changed, but you still pass the composed reducer to provideStore:

import { compose } from "@ngrx/core/compose";
import { StoreModule } from "@ngrx/store";

@NgModule({
    ...
    imports: [
        ...
        StoreModule.provideStore(compose(...))
    ],
    ...
cartant
  • 57,105
  • 17
  • 163
  • 197
15

With @ngrx/store": "^4.0.3" this is slightly different because there are small changes, so my 'clear state' looks like this

import { ActionReducerMap } from '@ngrx/store';
import { ActionReducer, MetaReducer } from '@ngrx/store';

export const rootReducer: ActionReducerMap<StoreStates> = {
  points: pointsReducer,
  ...
};

export function clearState(reducer: ActionReducer<StoreStates>): ActionReducer<StoreStates> {
  return function(state: StoreStates, action: Action): StoreStates {
    if (action.type === 'CLEAR_STATE') {
      state = undefined;
    }
    return reducer(state, action);
  };
}
export const metaReducers: MetaReducer<StoreStates>[] = [clearState];

and

import { StoreModule } from '@ngrx/store';
import { metaReducers, rootReducer } from '../root.reducer';

export const imports: any = [
   StoreModule.forRoot(rootReducer, { metaReducers }),
   ...
]
Limarenko Denis
  • 769
  • 1
  • 8
  • 13
  • 1
    How is this going to work when the app is organized into modules (with lazy loading), and each of the module has its own feature state? – user2960993 Jul 28 '18 at 16:58
  • It works with lazy loading as well, I mean I've got all my feature module lazy loading but the service and the store are in a shared module that I put in the app module – Whisher Aug 08 '18 at 19:31
  • BTW the CLEAR_STATE action in my case is LOGOUT ACTION) – Whisher Aug 08 '18 at 19:41
3

This isn't really an answer but the comments won't let me format it correctly. To add to what cartant said, if you are setting up your types like this:

export const ActionTypes = {
  LOGOUT:  type('[Environment] Logout of portal'),
  ....
}

It's the long description that you should use. Also if you name your root reducer rootReducer instead of just reducer then you would change that as well. Following is an edited example:

(I left this function within my root reducer)

const developmentReducer: ActionReducer<State> = compose(...DEV_REDUCERS,
(rootReducer: Function) => {
    return function(state, action) {
      if (action.type === '[Environment] Logout of portal') {
        state = undefined;
      }
      return rootReducer(state, action);
    };
  }, combineReducers)(reducers);
Post Impatica
  • 14,999
  • 9
  • 67
  • 78
0

[UPDATE 2022]

This blog post works like a charm for Angular 14 and very easy to implement. You can use any action to reset store. https://www.purcellyoon.com/insights/articles/angular-ngrx-8-reset-state-using-meta-reducer

Define logout action in actions.ts

import { createAction, props } from '@ngrx/store';

export const logoutSuccess = createAction('[Authentication] Logout Success');

Define your MetaReducer in reducers.ts

import { ActionReducer, ActionReducerMap, INIT, MetaReducer } from '@ngrx/store';

import * as AuthActions from './actions';

export const reducers: ActionReducerMap<State> = {
    [featureKey1]: reducer1,
    [featureKey2]: reducer2,
    //...

    }

export const logout = (reducer: ActionReducer<any>): ActionReducer<any> => {
  return (state, action) => {
    if (action !== null && action.type === AuthActions.logoutSuccess.type) {
      return reducer(undefined, { type: INIT });
    }
    return reducer(state, action);
  };
};

export const metaReducers: MetaReducer[] = [logout];

app.store.module.ts

import { metaReducers, reducers } from '../store/reducers';

@NgModule({
  imports: [
    StoreModule.forRoot(reducers, {
      metaReducers,
      runtimeChecks: {
        strictStateImmutability: true,
        strictActionImmutability: true
      }
    }),

Now dispatch your action in the component

this.store.dispatch(logout());

In effects, dispatch logout success action so that the user logs out and the store is reset.

logout$ = createEffect(() => this.actions$.pipe(
      ofType(Actions.logout),
      mergeMap((action) =>
        this.apiService.logout().pipe(
          map((user: User) => Actions.logoutSuccess()),
          catchError((error) => of(Actions.logoutFailure({ error })))
        )
      )
    ));
Balalayka
  • 187
  • 2
  • 15
  • This code is so incomplete it doesn't really help. Your reducer is named 'logout', and then you dispatch that reducer instead of some action? Just a confusing mess. – Frotty Jan 23 '23 at 11:10