Search code examples
angularnativescriptngxs

NGXS: How to use Store or set State in meta-reducer


I need to dispatch an action from within a meta-reducer or a plugin. I get the following errors when I add this provider to the App Module:

   {
       provide: NGXS_PLUGINS,
       useFactory: myfunction,
       deps: [Store],
       multi: true
    }

Cannot instantiate cyclic dependency! InternalStateOperations ("[ERROR ->]"): in NgModule AppModule

Cannot instantiate cyclic dependency! StateFactory ("[ERROR ->]"): in NgModule AppModule

What is the proper way to do it?

The meta-reducer is:

export function extendApplication(store: Store) {
  return function extendApplication(state, action, next) {
  if (state.user.loggedIn){

    if (getActionTypeFromInstance(action) !== LogoutUser.type) {

      here is where I want to set a timer and if no other actions
      occur before the timer expires I want to dispatch a logout action

      return next(state, action);
    }
  }else{
    return next(state, action);
  }}

The module has the above provider.


Solution

  • MetaReducers can be implemented via a function or a Service (Class).

    If you implement it via a function, you can do:

    import { NgModule } from '@angular/core';
    import { NGXS_PLUGINS } from '@ngxs/store';
    import { getActionTypeFromInstance } from '@ngxs/store';
    
    @NgModule({
      imports: [NgxsModule.forRoot([])],
      providers: [
        {
          provide: NGXS_PLUGINS,
          useValue: logoutPlugin,
          multi: true
        }
      ]
    })
    export class AppModule {}
    
    export function logoutPlugin(state, action, next) {
      // Use the get action type helper to determine the type
      if (getActionTypeFromInstance(action) === Logout.type) {
        // if we are a logout type, lets erase all the state
        state = {};
      }
    
      // return the next function with the empty state
      return next(state, action);
    }
    
    

    State is mutated just by updating the state object passed into the function and passing it back to the returned next function.

    You can inject the Store in the plugin, using Injector and getting the instance, but you can't dispatch an actiom inside the plugin, because you'll create an infinite loop.

    If you want to implement it via a Service, you can do:

    import {
      NGXS_PLUGINS,
      NgxsModule,
      ActionType,
      NgxsNextPluginFn,
      NgxsPlugin
    } from "@ngxs/store";
    import { Injectable, Inject, Injector } from '@angular/core';
    
    @NgModule({
      imports: [
        NgxsModule.forRoot([TestState]),
      ],
      providers: [
        {
          provide: NGXS_PLUGINS,
          useClass: TestInterceptor,
          multi: true
        }
      ]
    })
    export class AppModule {}
    
    @Injectable()
    export class TestInterceptor implements NgxsPlugin {
    
      constructor(
        private _injector: Injector
      ){
      }
    
      public handle(
        state,
        action: ActionType,
        next: NgxsNextPluginFn
      ): NgxsNextPluginFn {
        const matches: (action: ActionType) => boolean = actionMatcher(action);
        const isInitialAction: boolean = matches(InitState) || matches(UpdateState);  
    
        // you can validate the action executed matches the one you are hooking into
        // and update state accordingly
    
        // state represents full state obj, if you need to update a specific state, 
        // you need to use the `name` from the @State definition
    
        state = { test: ["state mutated in plugin"] };
    
        // get store instance via Injector 
        const store = this._injector.get<Store>(Store);
    
        return next(state, action);
      }
    }
    
    
    

    I also created stackblitz example if you'd like to check it out