Book Image

Architecting Angular Applications with Redux, RxJS, and NgRx

Book Image

Architecting Angular Applications with Redux, RxJS, and NgRx

Overview of this book

Managing the state of large-scale web applications is a highly challenging task with the need to align different components, backends, and web workers harmoniously. When it comes to Angular, you can use NgRx, which combines the simplicity of Redux with the reactive programming power of RxJS to build your application architecture, making your code elegant and easy to reason about, debug, and test. In this book, we start by looking at the different ways of architecting Angular applications and some of the patterns that are involved in it. This will be followed by a discussion on one-way data flow, the Flux pattern, and the origin of Redux. The book introduces you to declarative programming or, more precisely, functional programming and talks about its advantages. We then move on to the reactive programming paradigm. Reactive programming is a concept heavily used in Angular and is at the core of NgRx. Later, we look at RxJS, as a library and master it. We thoroughly describe how Redux works and how to implement it from scratch. The two last chapters of the book cover everything NgRx has to offer in terms of core functionality and supporting libraries, including how to build a micro implementation of NgRx. This book will empower you to not only use Redux and NgRx to the fullest, but also feel confident in building your own version, should you need it.
Table of Contents (12 chapters)

Dependency Injection

Essentially, when we ask for a construct instance, we want help constructing it. A DI system can act in one of two ways when asked to resolve an instance:

  • Transient mode: The dependency is always created anew
  • Singleton mode: The dependency is reused

Angular only creates singletons though which means every time we ask for a dependency it will only be created once and we will be given an already existing dependency if we are not the first construct to ask for that dependency.

The default behavior of any DI framework is to use the default constructor on a class and create an instance from a class. If that class has dependencies, then it has to resolve those first. Imagine we have the following case:

export class Logger { }

export class Service {
constructor(logger: Logger) { }
}

The DI framework would crawl the chain of dependencies, find the construct that does not have any dependencies, and instantiate that first. Then it would crawl upwards and finally resolve the construct you asked for. So with this code:

import { Service } from './service';

export class ExampleComponent {
constructor(srv: Service) { }
}

The DI framework would:

  • Instantiate the logger first
  • Instantiate the service second
  • Instantiate the component third

Dependency Injection in Angular using providers

So far we have only discussed Dependency Injection in general, but Angular has some constructs, or decorators, to ensure that Dependency Injection does its job. First imagine a simple scenario, a service with no dependencies:

export class SimpleService {}

If a component exists that requires an instance of the service, like so:

@Component({
selector: 'component'
})
export class ExampleComponent {
constructor(srv: Service) {}
}

The Angular Dependency Injection system comes in and attempts to resolve it. Because the service has no dependencies, the solution is as simple as instantiating Service, and Angular does this for us. However, we need to tell Angular about this construct for the DI machinery to work. The thing that needs to know this is called a provider. Both Angular modules and components have access to a providers array that we can add the Service construct to. A word on this though. Since the arrival of Angular modules, the recommendation is to not use the providers array for components. The below paragraphs are merely there to inform you how providers for components work.

This will ensure that a Service instance is being created and injected at the right place, when asked for. Let's tell an Angular module about a service construct:

import { Service } from "./Service";

@NgModule({
providers: [Service]
})
export class FeatureModule{}

This is usually enough to make it work. You can, however, register the Service construct with the component class instead. It looks identical:

@Component({
providers: [Service]
})
export ExampleComponent {}

This has a different effect though. You will tell the DI machinery about this construct and it will be able to resolve it. There is a limitation, however. It will only be able to resolve it for this component and all its view child components. Some may see this as a way of limiting what components can see what services and therefore see it as a feature. Let me explain that by showing when the DI machinery can figure out our provided service:

Everybody's parent – it works: Here, we can see that as long as the component highest up declares Service as a provider, all the following components are able to inject Service:

AppComponent // Service added here, Can resolve Service
TodosComponent // Can resolve Service
TodoComponent // Can resolve Service

Let's exemplify this with some code:

// example code on how DI for works for Component providers, there is no file for it
// app.component.ts
@Component({
providers: [Service] // < - provided,
template : `<todos></todos>`
})
export class AppComponent {}

// todos.component.ts
@Component({
template : `<todo></todo>`,
selector: 'todos'
})
export class TodosComponent {
// this works
constructor(private service: Service) {}
}

// todo.component.ts
@Component({
selector: 'todo',
template: `todo component `
})
export class TodoComponent {
// this works
constructor(private service: Service) {}
}

TodosComponent – will work for its children but not higher up: Here, we provide Service one level down, to TodosComponent. This makes Service available to the child components of TodosComponent but AppComponent, its parent, misses out:

AppComponent // Does not know about Service
TodosComponent // Service added here, Can resolve Service
TodoComponent // Can resolve Service

Let's try to show this in code:

// this is example code on how it works, there is no file for it
// app.component.ts
@Component({
selector: 'app',
template: `<todos></todos>`
})
export class AppComponent {
// does NOT work, only TodosComponent and below knows about Service
constructor(private service: Service) {}
}

// todos.component.ts
@Component({
selector: 'todos',
template: `<todo></todo>`
providers: [Service]
})
export class TodosComponent {
// this works
constructor(private service: Service) {}
}

// todo.component.ts
@Component({
selector: 'todo',
template: `a todo`
})
export class TodoComponent {
// this works
constructor(private service: Service) {}
}

We can see here that adding our Service to a component's providers array has limitations. Adding it to an Angular module is the sure way to ensure it can be resolved by all constructs residing inside of that array. This is not all though. Adding our Service to an Angular module's providers array ensures it is accessible throughout our entire application. How is that possible, you ask? It has to do with the module system itself. Imagine we have the following Angular modules in our application:

AppModule
SharedModule

For it to be possible to use our SharedModule, we need to import it into AppModule by adding it to the imports array of AppModule, like so:

//app.module.ts

@NgModule({
imports: [ SharedModule ],
providers: [ AppService ]
})
export class AppModule{}

We know this has the effect of pulling all constructs from the exports array in SharedModule, but this will also concatenate the providers array from SharedModule to that of AppModule. Imagine SharedModule looking something like this:

//shared.module.ts

@NgModule({
providers : [ SharedService ]
})
export class SharedModule {}

After the import has taken place, the combined providers array now contains:

  • AppService
  • SharedService

So the rule of thumb here is if you want to expose a service to your application, then put it in the Angular module's providers array. If you want to limit access to the service, then place it into a component's providers array. Then, you will ensure it can only be reached by that component and its view children.

Up next, let's talk about cases when you want to override the injection.

Overriding an existing construct

There are cases when you want to override the default resolution of your construct. You can do so at the module level, but also at the component level. What you do is simply express which construct you are overriding and with which other construct. It looks like this:

@Component({
providers: [
{ provide: Service, useClass : FakeService }
]
})

The provide is our known construct and useClass is what it should point to instead. Let's imagine we implemented our Service like so:

export class Service {
no: number = 0;
constructor() {}
}

And we added the following override to a component:

@Component({
providers: [{ provide : Service, useClass: FakeService }]
})

The FakeService class has the following implementation:

export class FakeService {
set no(value) {
// do nothing
}

get no() {
return 99;
}
}

Now the component and all its view child components will always get FakeService when asking for the Service construct.

Overriding at runtime

There is a way to decide what to inject for/into a construct at runtime. So far, we have been very explicit about when to override, but we can do this with a bit of logic added to it by using the useFactory keyword. It works like the following:

let factory = () => {
if(condition) {
return new FakeService();
} else {
return new Service();
}
}

@Component({
providers : [
{ provide : Service, useFactory : factory }
]
})

This factory can in itself have dependencies; we specify those dependencies with the deps keyword like so:

let factory = (auth:AuthService, logger: Logger) => {
if(condition) {
return new FakeService();
} else {
return new Service();
}
}

@Component({
providers : [
{ provide : Service, useFactory : factory,
deps: [AuthService, Logger] }
]
})

Here, we highlighted the condition variable, which is a Boolean. There can be a ton of reasons why we would want to be able to switch the implementation. One good case is when the endpoint don't exist yet and we want to ensure it calls our FakeService instead. Another reason could be that we are in testing mode and by just changing this one variable we can make all our services rely on a fake version of themselves.

Overriding constants

Not everything, though, is a class that needs to be resolved; sometimes it is a constant. For those cases, instead of using useClass, we can use useValue, like so:

providers: [ { provide: 'a-string-token', useValue: 12345678 } ]

This is not really a class type, so you can't write this in a constructor:

constructor(a-string-token) . // will not compile

That wouldn't compile. What we can do instead is to use the @Inject decorator in the following way:

constructor( @Inject('a-string-token') token) // token will have value 12345678

The useValue is no different from useClass when it comes to how to override it. The difference is of course that we need to type useValue in our instruction to override rather than useClass.

Resolving your dependencies with @Injectable

We took a little deep dive into DI in the previous section, but almost forgot about a very important decorator, @Injectable. @Injectable is not strictly mandatory to use for services in general. However, if that service has dependencies, then it needs to be used. Failure to decorate a service with @Injectable that has dependencies leads to an error where the compiler complains that it doesn't know how to construct the mentioned service. Let's look at a case where we need to use the @Injectable decorator:

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

@Injectable()
export class Service {
constructor(logger:Logger) {}
}

In this case, Angular's DI machinery will look up Logger and inject it into the Service constructor. So, providing we have done this:

providers: [Service, Logger]

In a component or module, it should work. Remember, when in doubt, add @Injectable to your service if it has dependencies in the constructor or will have in the near future. If your service lacks the @Injectable keyword and you try to inject it into a component's constructor, then it will throw an error and your component will not be created.

This section set out to explain how DI works from a general standpoint and how it works in Angular. For the latter, it covered how to register constructs to work with Angular's DI machinery, but also how to override it. It is clear that the DI machinery is quite sophisticated. It can be scoped to the application level, by adding constructs to the providers array of Angular modules, but also to the component level and its view children. The main reason for describing the DI machinery was to teach you the possibilities of it, so you know how to best use it to your advantage when you define the architecture of your app.