Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Taraneh Kossari
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Taraneh Kossari

Providing Services As Both A Multi-Collection And As An Individual Injectable In Angular 5.1.0

By Ben Nadel on

I've been thinking a lot of Redux and NgRx lately. And, one thing that keeps tripping me up is the large amount of functional composition that these libraries use what with some function being consumed by a function which is being consumed by a function which is being consumed by some other function. The mental map needed to understand the dependency chain requires a tremendous amount of cognitive load. And, I wonder if it might be possible to lean on the Dependency-Injection (DI) container in order to reduce the amount of indirection. But, before I think more deeply about this topic, I need to confirm that I can provide a service, in the DI container, as both a multi-collection and as an individual injectable.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

If an Angular service can be provided as both a multi-collection and as an individual injectable, it means that we can easily provide a set of homogenous types to a single point of oversight, like a "Store Manager", while, at the same time, being able to inject a specific portion of that store into a View component. This would allow inter-dependent services to be injected without having to create levels of indirection through the use of factory functions and complex abstractions.

To test this, I defined two services that implement the Greeter interface:

  • // Import the core angular services.
  • import { InjectionToken } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // I am the Dependency-Injection (DI) token for the Greeter collection.
  • export var GREETERS = new InjectionToken<Greeter[]>( "Greeter[] Multi Token" );
  •  
  • export interface Greeter {
  • greet( name: string ) : string;
  • }
  •  
  • export class NiceGreeter implements Greeter {
  •  
  • public greet( name: string ) : string {
  •  
  • return( `Hello ${ name }, so nice to meet you.` );
  •  
  • }
  •  
  • }
  •  
  • export class MeanGreeter implements Greeter {
  •  
  • public greet( name: string ) : string {
  •  
  • return( `What evs ${ name }, talk to the hand!` );
  •  
  • }
  •  
  • }

Notice that I am also exporting an InjectionToken - GREETERS - which will be used in the NgModule to define a multi-collection that contains the instances of the above Greeter implementations. Now, in the App module, we can try to provide these Greeter implementations as both individual services and as part of the GREETERS collection:

  • // Import the core angular services.
  • import { BrowserModule } from "@angular/platform-browser";
  • import { NgModule } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { AppComponent } from "./app.component";
  • import { GREETERS } from "./greeters";
  • import { MeanGreeter } from "./greeters";
  • import { NiceGreeter } from "./greeters";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • bootstrap: [
  • AppComponent
  • ],
  • imports: [
  • BrowserModule
  • ],
  • declarations: [
  • AppComponent
  • ],
  • providers: [
  • // First, we're going to provide the greeters as individually injectable
  • // services. This way, each one can be referenced directly, if needed.
  • MeanGreeter,
  • NiceGreeter,
  •  
  • // Next, we're going to provide the AFOREMENTIONED greeters as a collection
  • // of services. This way, the two services can be injected as a single set of
  • // services that implement the Greeter interface. By using the "useExisting"
  • // configuration, the instances associated with the preceding tokens will be
  • // used, rather than creating new instances of each service.
  • {
  • provide: GREETERS,
  • multi: true,
  • useExisting: MeanGreeter // ... use instance already in the DI container.
  • },
  • {
  • provide: GREETERS,
  • multi: true,
  • useExisting: NiceGreeter // ... use instance already in the DI container.
  • }
  • ]
  • })
  • export class AppModule {
  • // ...
  • }

The most important part of this module is that I am using the "useExisting" configuration when defining the GREETERS multi-collection. This tells Angular to look in the DI container for existing instances of the MeanGreeter and NiceGreeter classes when building-up the GREETERS collection. This should allow each Greeter service to be referenced directly as well as in aggregate.

And, to test this aggregation, we can inject both the individual services and the GREETERS collection into the AppComponent and then compare the in-memory object references:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { Inject } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { Greeter } from "./greeters";
  • import { GREETERS } from "./greeters";
  • import { MeanGreeter } from "./greeters";
  • import { NiceGreeter } from "./greeters";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <em>Look at console-logging - thats where the hot DI action is.</em>
  • `
  • })
  • export class AppComponent {
  •  
  • // I initialize the app component. Notice that we are injecting BOTH the collection
  • // of Greeters as well as the individual instances. We can then confirm that the
  • // collection of Greeters is a mere aggregation of the individual references.
  • constructor(
  • @Inject( GREETERS ) greeters: Greeter[],
  • meanGreeter: MeanGreeter,
  • niceGreeter: NiceGreeter
  • ) {
  •  
  • console.group( "@Inject( GREETERS )" );
  • console.log( "Count:", greeters.length );
  • console.log( greeters[ 0 ] );
  • console.log( greeters[ 1 ] );
  • console.groupEnd();
  •  
  • console.group( "MeanGreeter" );
  • console.log( meanGreeter );
  • console.log( "=== greeters[ 0 ]:", ( meanGreeter === greeters[ 0 ] ) );
  • console.log( "=== greeters[ 1 ]:", ( meanGreeter === greeters[ 1 ] ) );
  • console.groupEnd();
  •  
  • console.group( "NiceGreeter" );
  • console.log( niceGreeter );
  • console.log( "=== greeters[ 0 ]:", ( niceGreeter === greeters[ 0 ] ) );
  • console.log( "=== greeters[ 1 ]:", ( niceGreeter === greeters[ 1 ] ) );
  • console.groupEnd();
  •  
  • }
  •  
  • }

As you can see, we're simply testing to see if the individual NiceGreeter and MeanGreeter injectables are in the GREETERS array. And, when we run the above code, we get the following console output:


 
 
 

 
 Providing a service as part of a collection using multi:true in your module configuration. 
 
 
 

As you can see, the individually-injected MeanGreeter instance is the same in-memory object reference as the first item in the GREETERS multi-collection. And, the individually-injected NiceGreeter instance is the same in-memory object reference as the second item in the GREETERS multi-collection. Which confirms that an Angular service can be provided as both an individual injectable and as a multi-collection.

As a final note, if you want to break this behavior, you can replace "useExisting" with "useClass" in the App module definition. This will create brand new instances of the two Greeter implementations for the multi-collection. I don't know why you'd want to do this; but, it's always good to understand how the dependency-injection work in Angular.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

I'm not sure I see how this relates to use of Redux. Can you clarify the connection?

Also, your comment about "functions being consumed by functions" confuses me a bit. Are you talking about reducers, or something else?

Reply to this Comment

@Mark,

As far as functions being consumed by functions, I was primarily thinking about Selector composition. Building selectors by consuming existing selectors - that sort of thing:

createSelector( existingSelectorA, existingSelectorB, mySelector )

Now to be fair, the primary goal of the post was to look at the mechanics of dependency-injection in Angular. Redux and NgRx Store are only the things that happen to get me thinking about this particular topic.

That said, I like to think of "state" as belonging to the "views." Meaning, I have a hard thinking about state as belonging to the "app" itself. The views belong to the app, and the state belongs to the views.

Part of why I find this a nice way to think about the architecture is because it makes it very clear when and how you can delete code. If a UI View is deleted, then all of the state that it "owned" by that UI is also deleted (including all the actions and reducers as well).

It also makes it obvious the danger of consuming state owned by another UI view. For example, if ViewA reaches into ViewB's state to borrow some of it, there becomes an obvious danger that if ViewB ever changes the way it models its own state, there's a good likelihood that ViewA's logic will break. I am not saying this is a bad thing -- I'm saying that ownership serves the purpose of providing caution to the consumer. "Proceed at your own peril" kind of thing.

Of course, take all of this with a grain of salt since I am still very much struggling to wrap my head around a "store" based architecture. It is very possible that in a few weeks, I'll look back on this thought as insane :)

But, as one final thought - pulling this back to the actual topic of the post - I thought that for each View, I could create a "store" service that would be injectable:

// ViewA
constructor( viewStore: ViewAStore ) { ... }

... and, at the same time, provide this store as a _collection_ of stores to some dispatch manager:

{ provide: STORES, multi:true, useExisting:ViewAStore }

... which could be injected into the store manager:

// StoreManager
constructor( @Inject( STORES ) stores: Store[] ) { ... }

You could then .dispatch() an action on the StoreManager, which would in turn, dispatch the action on each of the injected Store implementations.

Anyway, just what I've been noodling on -- like I said, I may turn around in a few weeks and think this was a crazy concept.

Reply to this Comment

Yeah, I've noted that there seems to be a split in how people view app architecture: "component-centric" and "app-centric".

The way I would think about it is that if some data _truly_ only belongs to a certain view, then it _probably_ doesn't belong in the store. However, if data _does_ need to be shared by multiple views, then it's an app-wide concern and should be in the store. It's then the job of the UI bindings layer (ie, React-Redux's `connect` function) to extract the relevant data from the store and reshape it as necessary for the "plain" component's needs, which is the point of the "container component" pattern in general.

I can agree that the most optimal selector structure performance-wise does involve building up multiple nested layers of selectors, so that you get correct memoization at all levels of nesting. You absolutely _can_ write standalone selectors that know how to dig into multiple levels of the state, like `return state.a.b.c.d`, but for best perf, you would want to only recalculate `d` if `c` changed, and so on up the chain. Use of the Reselect library does tend towards writing each level's selectors by hand, but there's plenty of other options for writing selectors as well.

Reply to this Comment

@Mark,

While I have you, let me ask you a question that I've been pondering lately - how do you differentiate client-side actions vs. server-side actions that are pushed through WebSockets. What I means is, the payloads of such actions would necessarily be different.

For example, if I "add a comment" on the client-side, I will have all the user-submitted data such as the actual "text" of the comment. However, if I am notified that another user added a comment in a different browser, the WebSocket event will likely contain less information, such as just a collection or relevant IDs (ex, comment ID, parentID, etc.). As such, the same action cannot be handled by the same reducer workflow.

Do you solve this with different action names? Example:

- ADD_COMMENT (client-side)
- COMMENT_ADDED (websocket-event)

Have you had to deal with this kind of thing?

Reply to this Comment

I haven't had to deal with that situation myself, but I can see a few different ways to approach it.

One would be, as you said, to have separate actions distinguishing between "stuff done by the local client" and "stuff done by someone else that we're getting notified about". A variation on that would be to have the server forward on the original action, but add an additional piece of info tagging it as "something done remotely" (like in a `meta` field on the action), and have the reducer logic check for that extra flag.

It's also very feasible to treat forwarded actions the same as local actions, if that fits your use case. In that situation, what you're really doing is synchronizing different remote stores based on dispatched actions. I've got a list of available store synchronization libs here: https://github.com/markerikson/redux-ecosystem-links/blob/master/store.md#synchronization .

Reply to this Comment

@Mark,

Ok cool -- I'll take a look at this libraries. The only thing I get concerned about with pushing too much data over WebSockets is that, depending on your vendor, you can hit a payload size limit. For example, I use PusherApp for a lot of my WebSocket stuff so I don't have manage any of it, and it has a size-limit on its payloads. But, it is pretty large, I think.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.