Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using Treeshakable Providers in Angular

DZone's Guide to

Using Treeshakable Providers in Angular

We take a look at the useClass, useValue, useFactory, and useExisting providers in Angular and how they can aid in development.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

In this post, I want to describe how to use the useClass, useValue, useFactory, useExisting providers in the new treeshakable providers from Angular.

After this blogpost, you should have an example of how to use those four providers and have an idea what to do with them in case they are a solution to some problems you might face when developing Angular applications.

Introduction

Everybody is talking about the providedIn property of the configuration object which can be passed to the Injectable() decorator of Angular services. Basically, this means that a service can provide itself to a specific injector and is treeshakeable. That means if the service is not used it will get shaken out to make your application smaller and faster.

But the providedIn property is only one property of many which can describe how your service should be provided to your application. There's also: 

Let's start by installing the AngularCLI and scaffolding a new project with ng new myNewPlayground and wait for it to finish. After it's done, we can cd into the folder with cd myNewPlayground and create a new service with ng generate service Test. The AngularCLI will create a new service for us that looks a little something like this:

@Injectable({
    providedIn: 'root',
})
export class TestService {
    constructor() {}
}

This is where we start.

Let's modify the service like this:

@Injectable({
    providedIn: 'root',
})
export class TestService {
    sayHello() {
        console.log(`From TestService --> Hello`);
    }
}

So we added a method which basically does nothing else than logging something out to the console. Nothing spectacular so far.

In our AppComponent let's use this service now.

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
})
export class AppComponent {
    title = 'myNewPlayground';

    constructor(private readonly testService: TestService) {
        testService.sayHello();
    }
}

Again, we are just using the service, calling the method which logs something out. We will use the method to see which service is going to be used later.

If we run that one with npm start we can see in the console that

From TestService --> Hello

is printed out.

UseClass

So we see the providedIn already works, which is great, but we can use useClasstoo in this case.

Let's add another class which is like the same service but has a different name.

export class TestService2 {
    sayHello() {
        console.log(`From TestService2 --> Hello`);
    }
}

Note that this is a normal TypeScript class. There is no Angular reference to this class, no decorator, nothing so far.

Let's use the useClass provider now. Modify the configuration object to our decorator in the following way:

export class TestService2 {
    sayHello() {
        console.log(`From TestService2 --> Hello`);
    }
}

@Injectable({
    providedIn: 'root',
    useClass: TestService2, // <-- add this line
})
export class TestService {
    sayHello() {
        console.log(`From TestService --> Hello`);
    }
}

We are telling Angular that if we are asking for this service anywhere in our application we want to use a different service instead! If we check the console we see that we are working with the instance of TestService2 now, we get the following output: 

From TestService2 --> Hello

Okay, so that is how we can switch services ‘under the hood.’

Let’s see what useFactory can do.

UseFactory

With useFactory we can use a factory at runtime to decide which kind of service we want to return if it got requested by any other class in our application.

Note that you do not want to change the method and/or property calls on your requesting instances when ServiceB is being returned instead of ServiceA. You could use interfaces as contracts and abstract classes here, for example.

So let's create a new service first which has the same method as the other services and returns a new instance in a function. Aditionally let's a a method as factory which we can pass to the useFactory provider.

export class TestService3 {
    sayHello() {
        console.log(`From TestService3 --> Hello`);
    }
}

export function xyzFactory() {
    return new TestService3();
}

export class TestService2 {
    // ...
}

@Injectable({
    providedIn: 'root',
    useFactory: xyzFactory,
})
export class TestService {
    sayHello() {
        console.log(`From TestService --> Hello`);
    }
}

Running that will result in printing the following on the console:

From TestService3 --> Hello

This factory pattern comes out of the box which is very powerful and gives you a lot of possibilities when it comes to cross platform development.

Adding Dependencies to the Factory

Sometimes you have to add some dependencies to the factory because you need it to decide whether to return serviceA or serviceB. However, you can add the dependencies with the deps property on the configuration object.

Let's assume that we have to have the HttpClient from @angular/common/httpinside our factory. We can simply add it to our deps property inside an array.

export class TestService3 {
    sayHello() {
        console.log(`From TestService3 --> Hello`);
    }
}

export function xyzFactory(http: HttpClient) {
    console.log(!!http);
    return new TestService3();
}

export class TestService2 {
    // ...
}

@Injectable({
    providedIn: 'root',
    useFactory: xyzFactory,
    deps: [HttpClient],
})
export class TestService {
    sayHello() {
        console.log(`From TestService --> Hello`);
    }
}

Do not forget to include the HttpClientModule in the app.module.ts in this case.

If we check the console now we can see

true
test.service.ts:6 From TestService3 --> Hello

Alright, so we can use the deps property like usual. Let’s look at useValue next.

UseValue

You might get the idea that in relation to the others useValue is providing a single value. This way you can pass single values around and inject them into your components, services, etc.

To test the service — remember we did not change our app.component at all until now — as the ‘value’ we could use a JavaScript object with a property on it, which is a function that's doing something. This is a value, too! So let's do this.

@Injectable({
    providedIn: 'root',
    useValue: {
        sayHello: function() {
            console.log('whuuuut??');
        },
    },
})
export class TestService {
    sayHello() {
        console.log(`From TestService --> Hello`);
    }
}

Our console in the browser now prints out

whuuuut??

So this works, too. Instead of the TestService we are passing a simple object and to not let our app.component crash we give it a function called sayHellowhich gets called instead of the function of our TestService.

Last but not least let us take a look into useExisting

UseExisting

Imagine you have a service, ServiceA, in your application which you want to update with ServiceB, but you can’t for some reason. It would be nice if you could work with this like you would an alias: everybody who is asking for ServiceA should get ServiceB and all the new code you write will use ServiceB anyway. That allows you to not have two services with the same interface in your app: an old one and a new one. You only have the new one.

We already know useClass. You could easily provide a ServiceB and everybody asking for ServiceA: useClass will come into play and provide ServiceBinstead. But this has the problem that you have two instances of your services in your application.

@Injectable({
    providedIn: 'root',
})
export class ServiceB {
    sayHello() {
        console.log(`From ServiceB --> Hello`);
    }
}

@Injectable({
    providedIn: 'root',
    useClass: ServiceB,
})
export class ServiceA {
    sayHello() {
        console.log(`From ServiceA --> Hello`);
    }
}

This will create two instances of your ServiceB class which might not be what you want. And this is where useExisting comes into play. Using that you can refer to an already existing service and so act as an alias. Keep in mind that this time your class has to be an Angular service with a decorator and not just a plain class like in the examples above. Let's use it and get our service names in again.

@Injectable({
    providedIn: 'root',
})
export class TestService2 {
    sayHello() {
        console.log(`From TestService2 --> Hello`);
    }
}

@Injectable({
    providedIn: 'root',
    useExisting: TestService2,
})
export class TestService {
    sayHello() {
        console.log(`From TestService --> Hello`);
    }
}

This will result in

From TestService2 --> Hello

on the console.

Testing

When it comes to testing, you do not need to provide the services in the testing module if you want to test a service in your unit tests.

Out of the box, the AngularCLI creates a unit test for you that looks like this.

import { TestBed, inject } from '@angular/core/testing';

import { TestService } from './test.service';

describe('TestService', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [TestService],
        });
    });

    it('should be created', inject([TestService], (service: TestService) => {
        expect(service).toBeTruthy();
    }));
});

Thanks to the treeshakeable providers we can refactor this code to the following:

import { TestBed } from '@angular/core/testing';
import { TestService } from './test.service';

describe('TestService', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({});
    });

    it('should be created', () => {
        const service = TestBed.get(TestService);
        expect(service).toBeTruthy();
    });
});

As we do not need the services in the providers array of the testing module, we can simply get it from TestBed as if we provided it as our modules;5 don’t use the provider-array anymore because of the new syntax of threeshakeable providers.

If you would like to change the class which is being used by the unit tests you still have to use the providers array with your specific useClass, etc., property

Testing With Optional Dependencies

So Angular searches for all the services which are providedIn: 'root'automatically. Let us take a look at optional dependencies and see how they behave.

@Injectable({
  providedIn: 'root',
})
export class TestServieWithoutHttp {
  constructor() {
    console.log(`TestServieWithoutHttp created`);
  }
}

@Injectable({
  providedIn: 'root',
})
export class OptionalServiceWithHttp {
  constructor(private http: HttpClient) {
    console.log(`OptionalServiceWithHttp created`);
  }
}

@Injectable({
  providedIn: 'root',
})
export class TestServiceWithHttp {
  constructor(private http: HttpClient) {
    console.log(`TestServiceWithHttp created`);
  }
}

// ...
@Injectable({
  providedIn: 'root',
})
export class ServiceToTest {
  constructor(
    private testServiceWithHttp: TestServiceWithHttp,
    @Optional() private optionalServiceWithHttp?: OptionalServiceWithHttp
  ) {}
}

See the @Optional() decorator we use in the ServiceToTest? So the ServiceToTest uses a service which gets injected with an HttpClient and an optional other service which also gets injected with an HttpClient.

Our test looks like this:

describe('ServiceToTest', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                {
                    provide: TestServiceWithHttp,
                    useClass: TestServieWithoutHttp,
                },
                ServiceToTest,
            ],
        });
    });

    it('should be created', () => {
        const serviceToTest: ServiceToTest = TestBed.get(ServiceToTest);
        expect(serviceToTest).toBeTruthy();
    });
});

And this simple test, if the service can get created, blows up with an error:

Error: StaticInjectorError(DynamicTestModule)[HttpClient]:
    StaticInjectorError(Platform: core)[HttpClient]:
        NullInjectorError: No provider for HttpClient!

But why is that? We do not want to mock anything HTTP specific here and exactly because of that we are using the useClass to switch the service, which relies on HTTP, to a service that does not rely on HTTP.

So it turns out that Angular searches for the optional dependencies as well and the OptionalServiceWithHttp also uses HTTP. As we do not provide the HttpClientTestingModule in the test, because our test should not rely on HTTP, the test blows up. We can solve the issue by mocking our optional dependencies as well.

describe('ServiceToTest', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                {
                    provide: TestServiceWithHttp,
                    useClass: TestServieWithoutHttp,
                },
                { provide: OptionalServiceWithHttp, useValue: null }, // See this line?
                ServiceToTest,
            ],
        });
    });

    it('should be created', () => {
        const serviceToTest: ServiceToTest = TestBed.get(ServiceToTest);
        expect(serviceToTest).toBeTruthy();
    });
});

Recap

  • useClass, useValue, useFactory, useExisting can be used with the new syntax mostly like before.
  • The TestBed is “pre-provided” with all dependencies declared with @Injectable({ providedIn: 'root' }).
  • That can be a big surprise if you thought you had to provide everything to TestBed
  • It can bite you if you don’t mock out optional dependencies, too.

Big thanks to @wardbell especially for pointing out the unit test scenarios.

Hope this helps!

Fabian

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
web dev ,tutorial ,angular ,treeshakable ,providers

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}