Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NgRx: How to use services in meta-reducers?

I use a custom middleware (meta-reducer) to print my ngrx-store each time an action is dipatched. I wrote my middleware directly inside app.module.ts (where else should I put it ?):

app.module.ts

// ...imports

// ...

// FIXME:
/**
 * console.log action and state(before action) each time an action is dipatched
 * @param reducer reducer
 */
export function debug(reducer: ActionReducer<AppState, Actions>): ActionReducer<AppState, Actions> {

    const logger = new LoggerService(); ////////////// ERROR \\\\\\\\\\\\\\

    return (state, action) => {

        logger.storeInfo('ACTION', action);
        logger.storeInfo('STATE', state);

        return reducer(state, action);
    };
}

export const metaReducers: MetaReducer<any>[] = [
    debug,
];

@NgModule({
    declarations: [AppComponent, LoggerServiceComponent],
    entryComponents: [],
    imports: [
        // ...
        StoreModule.forRoot(reducers, { metaReducers }),
        StoreRouterConnectingModule.forRoot(), // Connects RouterModule with StoreModule
    ],
    providers: [
       // ...
    ],
    bootstrap: [AppComponent],
})
export class AppModule {}

There is an error because my LoggerService has a store injected (because I want all my logs to be stored in ngx-store. But I can't access the store neither !. Furthermore, I'm sure that's not the good way of accessing the singleton instance of a service...

  • Should I put my meta reducer inside a class, and then accessing the function of that class, but how do I do it ?
  • Is there a general method to access any service, like SomeClass.getServiceInstance(type) ?
  • Do you have other ideas on how to do it ?

[EDIT 1] - using META_REDUCERS (1st try - failing)

app.module.ts

import { LoggerService } from './services/logger.service';
import { AppState, Actions } from './app.state';
import { StoreModule, MetaReducer, ActionReducer, META_REDUCERS } from '@ngrx/store';

/**
 * Injects a `LoggerService` inside a `MetaReducer`
 * @param logger a service that allows to log and store console.log() messages
 * @returns a `MetaReducer`
 */
function debugFactory(logger: LoggerService): MetaReducer<AppState> {
    return (reducer: ActionReducer<AppState, Actions>): ActionReducer<AppState, Actions> => {
        return (state, action) => {

           logger.storeInfo('ACTION', action);
           logger.storeInfo('STATE', state);

           return reducer(state, action);
        };
    };
}

/**
 * Injects a LoggerService inside the debug `MetaReducer` function
 * @param logger a service that allows to log and store console.log() messages
 * @returns A list of `MetaReducer`
 */
export function getMetaReducers(logger: LoggerService): MetaReducer<AppState>[] {
    return [debugFactory(logger)];
}

const reducers = {
    layout: layoutReducer,
    preferences: preferencesReducer,
    router: routerReducer,
    debug: debugReducer,
};

@NgModule({
    declarations: [AppComponent ],
    entryComponents: [],
    imports: [
        // ...
        StoreModule.forRoot(reducers),
        StoreRouterConnectingModule.forRoot(), // Connects RouterModule with StoreModule
    ],
    providers: [
        // ...
        {
            provide: META_REDUCERS,
            deps: [LoggerService],
            useFactory: getMetaReducers,
            multi: true,
        },
    ],
    bootstrap: [AppComponent],
})
export class AppModule {}

This should work, according to the document but I have the following error at runtime:

TypeError: "fn is not a function"

Corresponding to this function (in the njrx-store library):

/**
 * @param {...?} functions
 * @return {?}
 */
function compose(...functions) {
    return (/**
     * @param {?} arg
     * @return {?}
     */
    function (arg) {
        if (functions.length === 0) {
            return arg;
        }
        /** @type {?} */
        const last = functions[functions.length - 1];
        /** @type {?} */
        const rest = functions.slice(0, -1);
        return rest.reduceRight((/**
         * @param {?} composed
         * @param {?} fn
         * @return {?}
         */
        (composed, fn) => {
             return fn(composed) // <----- HERE
        }), last(arg));
    });
}

In the debugger, it shows that the function array (...functions) contains some functions and one array, which I suspect is the result of the method getMetaReducers. I suspect either the example is wrong or there is a problem with the implementation of the compose method.

Tell me if you see any wrong things in my code.

[EDIT 2] - using USER_PROVIDED_META_REDUCERS as mentioned in answer (2nd try - failing)

code that have been edited

// OLD

    providers: [
        // ...
        {
            provide: META_REDUCERS,
            deps: [LoggerService],
            useFactory: getMetaReducers,
            multi: true,
        },
    ],

// NEW

    providers: [
        // ...
        {
            provide: USER_PROVIDED_META_REDUCERS,
            deps: [LoggerService],
            useFactory: getMetaReducers,
        },
    ],

It seems that my LoggerService is either not correctly passed nor initialized because I have now this error:

core.js:9110 ERROR TypeError: Cannot read property 'storeInfo' of undefined
    at http://localhost:8102/main.js:636:20
    at http://localhost:8102/vendor.js:109798:20
    at computeNextEntry (http://localhost:8102/vendor.js:108628:21)
    at recomputeStates (http://localhost:8102/vendor.js:108681:15)
    at http://localhost:8102/vendor.js:109029:26
    at ScanSubscriber.StoreDevtools.liftedAction$.pipe.Object.state [as accumulator] (http://localhost:8102/vendor.js:109081:38)
    at ScanSubscriber._tryNext (http://localhost:8102/vendor.js:120261:27)
    at ScanSubscriber._next (http://localhost:8102/vendor.js:120254:25)
    at ScanSubscriber.next (http://localhost:8102/vendor.js:114391:18)
    at WithLatestFromSubscriber._next (http://localhost:8102/vendor.js:122330:34)

if I comment those lines:

        logger.storeInfo('ACTION', action);
        logger.storeInfo('STATE', state);

No exception will be thrown, but my logger won't work either.

But at least the store configures itself correcly, the problem now is just that the LoggerService is either not correctly passed nor initialized. I guess I'm still doing something wrong

like image 265
David Alvarez Avatar asked Sep 23 '19 16:09

David Alvarez


Video Answer


1 Answers

You should try by injecting meta-reducers using the META_REDUCERS token as documented:

(HEADS UP: as of 24.Sept the docs are a bit misleading, the factory method return type should be MetaReducer and not MetaReducer[]. Check this example in the codebase)

export debugFactory(logger: LoggerService): MetaReducer<AppState> {
 return (reducer: ActionReducer<AppState, Actions>): ActionReducer<AppState, Actions> => {
    return (state, action) => {

        logger.storeInfo('ACTION', action);
        logger.storeInfo('STATE', state);

        return reducer(state, action);
    };
  }
}

@NgModule({
  providers: [
    {
      provide: META_REDUCERS,
      deps: [LoggerService],
      useFactory: debugFactory,
      multi: true
    },
  ],
})
export class AppModule {}

Update:

Since v8 you can use the ÙSER_PROVIDED_META_REDUCERS token:

export function getMetaReducers(logger: LoggerService): MetaReducer<AppState>[] {
 return [debugFactory(logger)];
}

providers: [
  {
     provide: USER_PROVIDED_META_REDUCERS,
     deps: [LoggerService],
     useFactory: getMetaReducers
  },
],

In this case the provided factory method has to return MetaReducer[]. Both "library" and "user" provided meta-reducers are combined into one collection in this function.

You will most likely hit a cyclic dependency between your store and logger service, as during the creation of the store, angular´s IOC container will try to instantiate a logger service, which depends on the store, before the creation of the store is finalized. You could solve this by "lazily" accessing the store in the logger service by injecting the Injector into it and defining a getter property for the store, something like:

private get() store{return this.injector.get(Store);}

ctor(private readonly injector: Injector){}
like image 199
Jota.Toledo Avatar answered Sep 25 '22 17:09

Jota.Toledo