Hugh Hou
Hugh Hou

Reputation: 2384

How to use other Angular2 service inside an ngrx/Store reducer?

New to both ngrx/Store and reducer. Basically, I have this reducer:

import {StoreData, INITIAL_STORE_DATA} from "../store-data";
import {Action} from "@ngrx/store";
import {
  USER_THREADS_LOADED_ACTION, UserThreadsLoadedAction, SEND_NEW_MESSAGE_ACTION,
  SendNewMessageAction
} from "../actions";
import * as _ from "lodash";
import {Message} from "../../shared-vh/model/message";
import {ThreadsService} from "../../shared-vh/services/threads.service";

export function storeData(state: StoreData = INITIAL_STORE_DATA, action: Action): StoreData {


  switch (action.type) {

    case SEND_NEW_MESSAGE_ACTION:
      return handleSendNewMessageAction(state, action);

    default:
      return state
  }
}

function handleSendNewMessageAction(state:StoreData, action:SendNewMessageAction): StoreData {

  const newStoreData = _.cloneDeep(state);

  const currentThread = newStoreData.threads[action.payload.threadId];

  const newMessage: Message = {
    text: action.payload.text,
    threadId: action.payload.threadId,
    timestamp: new Date().getTime(),
    participantId: action.payload.participantId,
    id: [need a function from this service: ThreadsService]
  }

  currentThread.messageIds.push(newMessage.id);

  newStoreData.messages[newMessage.id] = newMessage;

  return newStoreData;
}

The problem is within the reducer function, I do not know how to inject an injectable service I created in a different file and use the function within it. The id part - I need to generate a firebase push ID using function like this.threadService.generateID() ...

But since this is a function, I do not have a constructor to use DI and I have no idea how to get functions within threadService!

Upvotes: 32

Views: 23193

Answers (2)

Adrian Moisa
Adrian Moisa

Reputation: 4363

After thinking a while about this I came up with this idea: What if I have a service full of pure functions which I don't want to keep in a global variable outside of angular like this:

export const fooBarService= {
    mapFooToBar: (foos: Foo[]): Bar[] => {
        let bars: Bar[];
        // Implementation here ...
        return bars;
    } 
}

I would like to have it as a service so I can easily pass it in the application without anybody freaking out that I don't use dependency injection:

@Injectable()
export class FooBarService{
    public mapFooToBar (foos: Foo[]): Bar[] {
        let bars: Bar[];
        // Implementation here ...
        return bars;
    } 
}

I can use ReflectiveInjector in order to get an instance of the service I need. Keep in mind that this injector is invoked before the main app is going live so it's really necessary to play nice and avoid keeping state in these services. And of course also because reducers really have to be pure (for your own sanity).

// <!> Play nice and use only services containing pure functions
var injector = ReflectiveInjector.resolveAndCreate([FooBarService]);
var fooBarService= injector.get(FooBarService);

// Due to changes in ngrx4 we need to define our own action with payload
export interface PayloadAction extends Action {
    payload: any
}

/**
 * Foo bar reducer
 */
export function fooBarReducer(
    state: FooBarState = initialState.fooBar, 
    action: PayloadAction
) {
    switch (action.type) {

        case fooBarActions.GET_FOOS_SUCCESS:
            return Object.assign({}, state, <FooBarState>{
                foos: action.payload,
                // No effects used, all nicelly done in the reducer in one shot
                bars: fooBarService.mapFooToBar (action.payload) 

            });

        default:
            return state;
    }

}

Using this setup I can use three types of service FooBarDataService, FooBarMapsService and FooBarLogicService. Data service calls the webapi and provides observables from the state store with the results. Map service is used to map foos to bars and Logic service is used to add the business logic in a separate layer. This way I can have tiny controllers that are used only to glue objects together and serve them to the templates. Almost no logic in controllers. And as a final touch, the resolvers can provide the state store data in the routes thus abstracting away the state store completely.

More details about ReflexiveInjector here.

Upvotes: 5

cartant
cartant

Reputation: 58410

There is no mechanism for injecting services into reducers. Reducers are supposed to be pure functions.

Instead, you should use ngrx/effects - which is the mechanism for implementing action side-effects. Effects listens for particular actions, perform some side-effect and then (optionally) emit further actions.

Typically, you would split your action into three: the request; the success response; and the error response. For example, you might use:

SEND_NEW_MESSAGE_REQ_ACTION
SEND_NEW_MESSAGE_RES_ACTION
SEND_NEW_MESSAGE_ERR_ACTION

And your effect would look something like this:

import { Injectable } from "@angular/core";
import { Actions, Effect, toPayload } from "@ngrx/effects";
import { Action } from "@ngrx/store";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map";

@Injectable()
export class ThreadEffects {

  constructor(
    private actions: Actions,
    private service: ThreadsService
  ) {}

  @Effect()
  sendNewMessage(): Observable<Action> {

    return this.actions
      .ofType(SEND_NEW_MESSAGE_REQ_ACTION)
      .map(toPayload)
      .map(payload => {
        try {
          return {
              type: SEND_NEW_MESSAGE_RES_ACTION,
              payload: {
                  id: service.someFunction(),
                  // ...
              }
          };
        } catch (error) {
          return {
              type: SEND_NEW_MESSAGE_ERR_ACTION
              payload: {
                error: error.toString(),
                // ...
              }
          };
        }
      });
  }
}

Rather than interacting with the service, your reducer would then be a pure function that would need only to handle the SEND_NEW_MESSAGE_RES_ACTION and SEND_NEW_MESSAGE_ERR_ACTION to do something appropriate with the success or error payloads.

Effects are observable-based, so incorporating synchronous, promise-based or observable-based services is straight forward.

There are some effects in the ngrx/example-app.

Regarding your queries in the comments:

The .map(toPayload) is just for convinience. toPayload is an ngrx function that exists so it can be passed to .map to extract the action's payload, that's all.

Calling a service that's observable-based is straight-forward. Typically, you'd do something like this:

import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/of";
import "rxjs/add/operator/catch";
import "rxjs/add/operator/map";
import "rxjs/add/operator/switchMap";

@Effect()
sendNewMessage(): Observable<Action> {

  return this.actions
    .ofType(SEND_NEW_MESSAGE_REQ_ACTION)
    .map(toPayload)
    .switchMap(payload => service.someFunctionReturningObservable(payload)
      .map(result => {
        type: SEND_NEW_MESSAGE_RES_ACTION,
        payload: {
          id: result.id,
          // ...
        }
      })
      .catch(error => Observable.of({
        type: SEND_NEW_MESSAGE_ERR_ACTION
        payload: {
          error: error.toString(),
          // ...
        }
      }))
    );
}

Also, effects can be declared as functions returning Observable<Action> or as properties of type Observable<Action>. If you are looking at other examples, you are likely to come across both forms.

Upvotes: 39

Related Questions