markelc
markelc

Reputation: 354

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.

Upvotes: 1

Views: 1610

Answers (2)

Michal
Michal

Reputation: 5226

EDIT: Actually it doesn't make sense to use middleware when you can use plugin that already has the middleware. This way all we have to do is create action:

@Action(RequestLogout)
async requestLogout(context: StateContext<IAuthStateModel>) {
  context.dispatch(new Navigate(['/login']));
  context.dispatch(new StateResetAll());
}

Middleware solution:

Technically the provided answer is not correct because {} doesn't reset app states to their default values.

For future reference for people who will be implementing this I offer following solution:

import { ActionType, getActionTypeFromInstance, NgxsNextPluginFn, NgxsPlugin, Store } from '@ngxs/store';
import { Injectable, Injector } from '@angular/core';
import { RequestLogout } from './auth.actions';
import { Navigate } from '@ngxs/router-plugin';
import { StateResetAll } from 'ngxs-reset-plugin';

@Injectable()
export class LogoutMiddleware implements NgxsPlugin {

  constructor(
    private injector: Injector,
  ) {
  }

  public handle(
    state: any,
    action: ActionType,
    next: NgxsNextPluginFn,
  ): NgxsNextPluginFn {
    if (getActionTypeFromInstance(action) === RequestLogout.type) {
      const store = this.injector.get<Store>(Store);
      store.dispatch(new Navigate(['/login']));
      store.dispatch(new StateResetAll());
    }

    return next(state, action);
  }
}

Then in your app module, you have to declare the LogoutMiddleware (fun fact meta-reducer is just fancy name for redux middleware) and import NgxsResetPluginModule.

providers: [
    {
      provide: NGXS_PLUGINS,
      useClass: LogoutMiddleware,
      multi: true,
    },
    ...
  ],
  imports: [
    NgxsResetPluginModule.forRoot(),
    ...
  ],

Upvotes: 0

joaqcid
joaqcid

Reputation: 133

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

Upvotes: 2

Related Questions