Experimenting with Angular 2 dependency injection

In this post I want to explore how the dependency injection (DI) system works a bit in Angular 2. Dependency Injection is a variant of the Inversion of Control (IoC) pattern, and is a powerful way to construct an application so as to reduce coupling and increase test-ability (good explanation). I do a lot of .NET development, and there are many, many IoC containers available in that world. I typically use Autofac because it's powerful but easy enough to reason about.

Autofac

One of the things I like about Autofac is it's very easy to control how a component is created when it needs to be injected somewhere. For example, assume I have a .NET class called Worker, then with Autofac I can write something like this:

var builder = new ContainerBuilder();  
builder.RegisterType<Worker>();  

With this registration Autofac will create a new instance of Worker any time something needs it. Now I can also indicate that I want to use the same instance of the class for every request (aka the Singleton):

var builder = new ContainerBuilder();  
builder.RegisterType<Worker>().SingleInstance();  

With that registration Autofac will create the object once, and re-use it for every request afterwards. The key point here is that in this context Autofac doesn't care what is requesting the object, or where in the system that object happens to be...it just responds to the request the same every time. Now there are more advanced scenarios that don't behave exactly like this, but they're not relevant to this article.

These examples are pulled right from the autofac docs , and I'm showing them just to provide a reference point as we explore how DI works with Angular 2...because the core concepts are not quite the same.

Angular 2

So to explore how DI works in Angular 2 I wanted to try to replicate the first example above, where I have some component in my system that takes a dependency, and every time that dependency is needed I wanted a new object. With Autofac this can be controlled purely in the Autofac registration code, and the object itself has no idea how its dependency is actually created. This is really nice because it further decouples the object from how its dependencies are provided (this is the inversion of control principle in action). With Angular 2 however, things are different because it uses a hierarchical injection system where every component is provided its own injector.

With Angular 2 a new injector is created for every component, and what it does when resolving any dependency it first checks if the injector for the component can provide it. If not it walks up the component tree and checks the parent component's injector if it can provide the dependency. If not, then it keeps going up the tree until something can provide it, or an error is thrown. A key aspect of this though is that at any given level a dependency is created as a singleton...there's no other option. The Angular 2 approach, albeit useful, throws a wrench into implementing the same behavior possible with Autofac.

To help me explore how these hierarchical injectors work I created a simple demo where I have a service that I will provide, to use Angular 2 terms, to a multiple instances of a component. That service has an ID value on it that helps me see how the instance of the service is shared among different components. It also has a method on it to regenerate the ID. Ok, enough words, let's see this in action:

In the demo you can see how clicking the Regenerate link, which is just calling a method on the service, causes the value for multiple instances of my components to change. This approach helps me see exactly what instances of the service are shared among components in the system. Let's break down the elements in this example.

The common stuff

Let's quickly lay out the top-level components that aren't really related to the demo itself. The service itself is pretty simple, and looks like this:

import {Injectable} from "@angular/core";

@Injectable()
export class IdService {  
    /**
     * An id value that is associated with this instance of the service
     */
    id: string;

    constructor() {
        this.regenerate();
    }

    /**
     * Updates this instance of the service with a new id value
     */
    regenerate(): void {
        this.id = this.generateUUID();
    }

    /**
     * A simple method to generate a GUID-like value that is (for our
     * purposes) unique every time.
     */
    private generateUUID(): string {
        var d = new Date().getTime();
        var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            var r = (d + Math.random() * 16) % 16 | 0;
            d = Math.floor(d / 16);
            return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
        return uuid;
    };
}

You can see it has an id property and a public method called regenerate(). This is what is called when the link is clicked in the demo. I'm not showing the main.ts here because it's pretty standard, but here's what my AppComponent looks like:

import {Component} from "@angular/core";

import {IdService} from "./id.service";  
import {SharedComponent} from "./shared.component";  
import {IsolatedComponent} from "./isolated.component";  
import {IndividualComponent} from "./individual.component";

@Component({
    selector: 'my-app',
    styles: [`        
        .subpanel {
            font-family: "Courier New", Courier, monospace;
            font-size: 0.9em;
            display: flex;
            flex-direction: column;
            margin: 20px;
            padding: 10px;
        }    
    `],
    template: `
        <h2>Angular 2 Dependency Injection demo</h2>

        <h3>These two child components share same service instance</h3>
        <shared class="subpanel"></shared>
        <shared class="subpanel"></shared>

        <h3>These share same instance, but isolated from the one above</h3>
        <isolated class="subpanel"></isolated>

        <h3>These each have their own service instance</h3>
        <individual class="subpanel"></individual>

    `,
    directives: [SharedComponent, IsolatedComponent, IndividualComponent],
    //
    // We provide the IdService in the AppComponent's injector so that any
    //  child component can get it, but it will be a single application-wide
    //  instance if this is the one used
    //
    providers: [IdService]
})
export class AppComponent { }  

Ok, let's break down each piece of the demo in more detail.

The top section of the demo

The top section of the demo consists of two instances of a component called SharedComponent, where each of those contain 4 instances of a component called ChildComponent. Here's a visual of the hierarchy:

In the demo you can see how clicking the Regenerate link causes the ID value to change for all of these. The reason is because of how we have defined the SharedComponent and the ChildComponent. Here's what the ChildComponent looks like:

import {Component} from "@angular/core";

import {IdService} from "./id.service";

@Component( {
    selector: 'child',
     styles: [`
        span {
            margin-left: 10px;
            margin-right: 10px;
        }
    `],
   template: `
        <span><a href="#" (click)="idService.regenerate()">Regenerate</a></span>
        <span>{{idService.id}}</span>
    `
})
export class ChildComponent {

    constructor(
        //
        // Here we indicate we need an instance of IdService injected, but
        //  since we're not "providing" it within this component, Angular
        //  will walk up the injector tree finding something that can
        //  
        private idService: IdService
    )
    {}
}

...and here's the SharedComponent:

import {Component} from "@angular/core";

import {ChildComponent} from "./child.component";

@Component({
    selector: 'shared',
    template: `
        <child></child>
        <child></child>
        <child></child>
        <child></child>
    `,
    directives: [ChildComponent]
})
export class SharedComponent { }  

So what happens here is each instance of ChildComponent is injected with the IdService class, but SharedComponent hasn't configured its injector to provide it, so Angular 2 will move up the component tree looking for something that has a provider defined and finds that at the highest level (our top-level application component). Since Angular 2 always creates singletons, it creates a single instance of the service at the top-level and it's thus shared among all the ChildComponent instances here.

The middle section of the demo

In the middle panel things look very similar to the top, but we can see how those instances of the ChildComponent are using their own instance of the service. Here's the component tree in that example:

Everything else is the same except that instead of the SharedComponent we have a new one called IsolatedComponent. The reason is to ensure its children get a new instance of the IdService it needs to be configured to do so:

import {Component} from "@angular/core";

import {IdService} from "./id.service";  
import {ChildComponent} from "./child.component";

@Component({
    selector: 'isolated',
    template: `
        <child></child>
        <child></child>
        <child></child>
        <child></child>
    `,
    directives: [ChildComponent],
    //
    // We don't want any children of this component to use the top-level
    //  instance of IdService, so we configure its injector to provide
    //  a new instance here. Note, this is still going to be a singleton
    //  so any children using this will share the same instance.
    //
    providers: [IdService]
})
export class IsolatedComponent { }  

Notice the providers: [IdService] section in there? That's the piece that tells Angular that at this level of the injector hierarchy I want to provide a new instance of the service. Again, Angular will create a single instance of the service, but now each of the children below this component in the tree hierarchy will share it. What's nice about this is the ChildComponent hasn't changed here. It's blissfully unaware of how or where that IdService is created, which is what we like, but that will change in a moment.

The bottom section of the demo

The bottom section is where I was really trying to achieve my original goal of having a unique instance of the service injected anywhere I want it. However, to achieve that I needed to create new versions of all my components. Here's the visual tree now:

So we have a new parent component called IndividualComponent, and that's required because it needs to use a new version of the child component. Here's the code:

import {Component, provide} from "@angular/core";

import {IndividualChildComponent} from "./individualChild.component";

@Component({
    selector: 'individual',
    template: `
        <child></child>
        <child></child>
        <child></child>
        <child></child>
    `,
    directives: [IndividualChildComponent]
})
export class IndividualComponent { }  

Note, we're not configuring its injector with anything special, so it won't be providing any new instances of the IdService. For this demo it doesn't matter though, because the only way to get each child a new instance of the IdService, they need to provide it themselves:

import {Component} from "@angular/core";

import {IdService} from "./id.service";

@Component({
    selector: 'child',
    styles: [`
        span {
            margin-left: 10px;
            margin-right: 10px;
        }
    `],
    template: `
        <span><a href="#" (click)="idService.regenerate()">Regenerate</a></span>
        <span>{{idService.id}}</span>
    `,
    //
    // The only way for these child components to get a new instance of these
    //  service is to configure their own injector to provide it. This ensures
    //  that no parent instance will be used. 
    //
    // However, it also means that this has to be a new component so we can
    //  use a new version of the @Component decorator that includes this 
    //  'providers' parameter.
    //
    providers: [IdService]
})
export class IndividualChildComponent{

    constructor(
        private idService: IdService
    ) {}
}
So what's the problem?

So why did I say earlier that the approach Angular 2 throws a wrench into how I'd like DI to work? The reason is the use of hierarchical injectors forces me to couple my components with the knowledge of how their dependencies are resolved. Ideally in this demo I'd really like to have a single ChildComponent that didn't have any knowledge about how its dependencies were resolved. With this hierarchical system though that isn't possible. Here I'm forced to re-implement this ChildComponent as IndividualChildComponent so I can specify the providers array and configure the injector to provide a new instance of IdService.

I'm still searching for a way around this, but just haven't found anything yet. I haven't run into any major cases where this has been a serious problem for me either, but I think it's something important to be aware of. I am sure as I build more complex Angular 2 apps situations will arise where I really won't want to duplicate a component to work around this design, but time will tell.

Wrapping up

In this article I dug a little bit into how the hierarchical injector system of Angular 2 works, and created a simple example to help me reason about it. I also tried to explore how to implement functionality common in other IoC systems available in .NET. As with any of my demos, all the code is available in my GitHub repo:

https://github.com/sstorie/experiments/tree/master/angular2-child-injectors

If you have any comments, or know of a way around the issue I described, please let me know in the comments!

comments powered by Disqus