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)

Using ES2015 modules

So far, we have mentioned that models are just plain classes. An ES2015 module is just one file. Within that file lives both public and private constructs. Things that are private are only visible within that file. Things that are public can be used outside said file. In Angular, Es2015 modules aren't used only for models but for all imaginable constructs such as components, Directives, Pipes, Services, and so on. This is because ES2015 modules are an answer to how we split our project into smaller parts, which provides us with the following benefits:

  • Many small files makes it easier to parallelize the work you do and have many developers work on it at the same time
  • The ability to hide data by, making some parts of your application public and some other private
  • Code reuse
  • Better maintainability

We have to remember what web development used to look like to understand these statements. When the web was young our JavaScript code more often than not consisted of one file. That quickly became a huge mess. There have been different techniques over the years to find a way to split up our app into many small files. Many small files have made it easier to maintain and also to get a good overview of what is going on, among many other benefits. There have been other issues though. As all these small files had to be stitched back together before being shipped with the app, a process called bundling, we suddenly had one giant file where functions and variables could by mistake affect each other due to naming collisions. A way to attack that problem is to deal with something called information hiding. This to ensure the variables and functions we created are only visible to certain other constructs. There are multiple ways, of course, to address this issue. ES2015 has a private by default way about them. Everything declared in an ES2015 is private by default unless you explicitly export it, thereby making it publicly accessible to other modules that import the aforementioned module.

So how does this connect to the previous statements? Any module system really allows us to maintain visibility in our project as it grows with us. The alternative is one file which is complete chaos. As for several developers working at the same time, any way of logically dividing up the app makes it easier to divide up the workstreams between developers.

Consuming a module

In ES2015, we use the import and from keywords to import one or several constructs like so:

import { SomeConstruct } from './module';

The imported file looks like this:

export let SomeConstruct = 5;

The basic operations involved, working with ES2015 modules, can be summarized as follows:

  • Define a module and write the business logic of the module
  • Export the constructs you want to make public
  • Consume said module with an import keyword from a consumer file

Of course there is a bit more to it than that, so let's look at what else you can do in the next subsection.

An Angular example

We have been using ES2015 imports extensively throughout this chapter already, but let's emphasize when that was. As mentioned, all constructs used ES2015 modules, models, services, components, and modules. For the module, this looked like this:

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

@NgModule({
declarations: [],
imports: [],
exports: [],
providers: []
})
export class FeatureModule {}

Here, we see that we import the functionality we need and we end up exporting this class, thereby making it available for other constructs to consume. It's the same thing with modules, like so:

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

@Component({
selector: 'example'
})
export class ExampleComponent {}

The pipe, directive, and filter all follow the same pattern of importing what they need and exporting themselves to be included as part of an NgModule.

Multiple exports

So far, we have only shown how to export one construct. It is possible to export multiple things from one module by adding an export keyword next to all constructs that you wish to export, like so:

export class Math {
add() {}
subtract() {}
}

export const PI = 3.14

Essentially, for everything you want to make public you need to add an export keyword at the start of it. There is an alternate syntax, where instead of adding an export keyword to every construct, we can instead define within curly brackets what constructs should be exported. It looks like this:

class Math {
add() {}
subtract() {}
}

const PI = 3.14

export {
Math, PI
}

Whether you put export in front of every construct or you place them all in an export {}, then end result is the same, it's just a matter of taste which one to use. To consume constructs from this module, we would type:

import { Math, PI } from './module';

Here, we have the option of specifying what we want to import. In the previous example, we have opted to export both Math and PI, but we could be content with only exporting Math, for example; it is up to us.

The default import/export

So far, we have been very explicit with what we import and what we export. We can, however, create a so-called default export, which looks somewhat different to consume:

export default class Player {
attack() {}
move() {}
}

export const PI = 3.13;

To consume this, we can write the following:

import Player from './module';
import { PI } from './module'

Note especially the first row where we no longer use the curly brackets, {}, to import a specific construct. We just use a name that we make up. In the second row, we have to name it correctly as PI, but in the first row we can choose the name. The player points to what we exported as default, that is, the Player class. As you can see, we can still use the normal curly brackets, {}, to import specific constructs if we want to.

Renaming imports

Sometimes we may get a collision, with constructs being named the same. We could have this happening:

import { productService } from './module1/service'
import { productService } from './module2/service'; // name collision

This is a situation we need to resolve. We can resolve it using the as keyword, like so:

import { productService as m1_productService }
import { productService as m2_productService }

Thanks to the as keyword, the compiler now has no problem differentiating what is what.

The service

We started this main section talking about how ES2015 modules are for all constructs in Angular. This section is about services, and services are no different when it comes to using ES2015 modules. Services we use should be declared in a separate file. If we intend to use a service, we need to import it. It needs to be imported for different reasons though, depending on what type of service it is. Services can be of two types:

  • Services without dependencies
  • Services with dependencies

Service without dependencies

A service without dependencies is a service whose constructor is empty:

export Service {
constructor(){}
getData() {}
}

To use it, you simply type:

import { Service } from './service'
let service = new Service();
service.getData();

Any module that consumes this service will get their own copy of the code, with this kind of code. If you, however, want consumers to share a common instance, you change the service module definition slightly to this:

class Service {
constructor() {}
getData() {}
}
const service = new Service();
export default service;

Here, we export an instance of the service rather than the service declaration.

Service with dependencies

A service with dependencies has dependencies in the constructor that we need help resolving. Without this resolution process, we can't create the service. Such a service may look like this:

export class Service {
constructor(
Logger logger: Logger,
repository:Repository
) {}
}

In this code, our service has two dependencies. Upon constructing a service, we need one Logger instance and one Repository instance. It would be entirely possible for us to find the Logger instance and Repository instance by typing something like this:

import { Service } from './service'
import logger from './logger';
import { Repository } from './repository';

// create the service
let service = new Service( logger, new Repository() )

This is absolutely possible to do. However, the code is a bit tedious to write every time I want a service instance. When you start to have 100s of classes with deep object dependencies, a DI system quickly pays off.

This is one thing a Dependency Injection library helps you with, even if it is not the main motivator behind its existence. The main motivator for a DI system is to create loose coupling between different parts of the system and rely on contracts rather than concrete implementations. Take our example with the service. There are two things a DI can help us with:

  • Switch out one concrete implementation for another
  • Easily test our construct

To show what I mean, let's first assume Logger and Repository are interfaces. Interfaces may be implemented differently by different concrete classes, like so:

import { Service } from './service'
import logger from './logger';
import { Repository } from './repository';

class FileLogger implements Logger {
log(message: string) {
// write to a file
}
}

class ConsoleLogger implements Logger {
log(message: string) {
console.log('message', message);
}
}

// create the service
let service = new Service( new FileLogger(), new Repository() )

This code shows how easy it is to switch out the implementation of Logger by just choosing FileLogger over ConsoleLogger or vice versa. The test case is also made a lot easier if you only rely on dependencies coming from the outside, so that everything can therefore be mocked.