angulardependency-injectionreduxngrx

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!


Solution

  • 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.