Data Flow in Angular 2 Applications

In this post we continue our study on how to build maintainable systems in Angular 2. We have already discussed the importance of proper code structure and good practices for dividing an application into components. However, a system is not just a static source code. Lots of the complexity comes from its runtime behavior, i.e. how different elements of the system interact with each other.

In this post we present how to design the data flow in an Angular 2 application. First we study possible ways how components can exchange information (and we show how to define good component APIs), then we take a broader look at the data flow in an entire system.

Component API

An Angular 2 component does not exist in isolation. It is usually a part of a bigger system, where multiple components need to exchange information between each other.  Thus, a component needs to specify a contract – what data it needs as input and what data it produces as output. In the following sections we discuss various ways to pass data to / retrieve data from a component.

Input

There are two main ways of passing data to an Angular 2 component:

  • pass it as an @Input parameter
  • inject it into the constructor

Both of them are useful, but also both of them have limitations and drawbacks.

@Input Parameters

Angular 2 provides a notion of @Input parameters. They are passed from a parent component. Whenever the parameter changes in the parent, the change is also visible in the child. The binding between parent and child happens in the template. Consider the following code:

@Component({
  selector: "parent",
  template: "<child [label]='text'></child>"
})
export class ParentComponent {
  private text = "foo";
  public ngOnInit() {
    setTimeout(() => this.text = "bar", 1000);
  }
}

@Component({
  selector: "child",
  template: "<div>Hello {{label}}</div>"
})
export class ChildComponent {
  @Input() label: string;
}

The child component will first display “Hello foo” and then after a second it will change to “Hello bar”. This is a powerful feature, but we often deliberately give it up and when a parameter is subject to change, we prefer to wrap it into an observable. In the tiny example above, the child component just displays what it got from the parent. In many practical cases, some additional processing is necessary (e.g. filtering of incoming values etc.) and it is much cleaner to use an observable than to implement the ngOnChanges lifecycle hook.

Properties of @Input Parameters

@Input parameters are a standard Angular 2 technique and at the beginning it might be tempting to use them to pass all data. We use it in the following cases:

  • the parameter represents state of the view. Sometimes a component has multiple visual states (e.g. default state, selected state etc.) and the choice of the state is only part of the visual layer (not the business logic)
  • the component is very generic and has minimal or no logic (and we want to keep its API as simple as possible). An example of such component could be a date picker.
  • the component needs to know which resource out of many it should use. Imagine that we have a UserComponent that represents details of a user. It needs to know which user it should show in a particular situation. The service layer should maintain the state of the model, but not the state of the view (e.g. which user’s details are currently displayed), thus this information needs to be passed explicitly via an @Input. As the calling component does not know (and should not know) which exactly data the called component might be interested in, we often pass only an identifier and let the called component grab the necessary data from a service.

We believe that @Input parameters have their drawbacks too. If we would use them to pass all the data needed in the entire system, we would probably break all loose coupling and high cohesion rules: parent component would need to precisely know what data the child component may need in its internal logic. Moreover, this problem would propagate up to the components tree root. Components would also have huge, not maintainable and messy APIs. They would expose @Input parameters that they don’t use and only pass through to the descendants. To limit the number of inputs, we often use another technique – injecting data services into component’s constructor.

Injecting Services into Constructor

Constructor arguments in components are resolved when the component is created and are provided by the dependency injection mechanism. We use this approach to inject services that provide data:

@Component({
  selector: "welcome",
  template: "<div>Hello {{name}}</div>"
})
export class WelcomeComponent {
  private name: string;
  constructor(userService: UserService) {
    this.name = userService.getUserName();
  }
}

In a more real-life scenario the service would perform some asynchronous computation and return an observable instead of a simple type.

Using Injection into Constructor

The data that your components use comes eventually from a backend system, thus at some point, some of the components need to pull them from an appropriate service (you don’t want to handle communication with the backend in your components). At Sparkbit, we often let each component pull the data it needs from an injected service (usually based on some identifier passed to that component via an @Input parameter).

Injecting services also has its drawbacks. The component gets an additional dependency and its potential for reuse becomes smaller. Imagine that we have a UserList component that we would like to include in our enterprise-wide component library so that we can reuse it in multiple applications. If the component uses some UserService, the service would need to be part of the library as well. It is however quite likely that different applications may integrate with different backend systems to obtain the user list.

In object-oriented programming, one of the key rules to build maintainable APIs, is to use interfaces instead of concrete implementations. We adopt this approach to our Angular 2 systems and we inject into our components service interfaces instead of service implementations. We try to keep these interfaces as limited as possible.

Output

Components often allow users to perform some actions and results of these actions need to be somehow communicated to the rest of the system. A component can act on behalf of another component, e.g. a date picker component should just let the user choose a date and it should return the date back to its parent. It should not be interested how the date will be used. The child component has several ways of passing data back to its parent.

@Output Events vs @Input Streams

Angular 2 introduces an @Output decorator. It is used to create custom event sources, on which the parent component can register a callback:

...
@Component({...})
export class HeartbeatComponent{
  @Output() public heartbeat = new EventEmitter<boolean>();
  public ngOnInit() {
    setInterval(() => this.heartbeat.emit(true, 500);
  }
}

@Component({
  template: `<heartbeat (heartbeat)="onValue($event)"></heartbeat>`
  ...
})
export class ParentComponent {
  private onValue(event) {
    console.log(event);
  }
}

We rarely use this mechanism in practice. When dealing with reactive streams of data, we prefer observables over callbacks, thus we would rather pass an RxJs Subject as an input parameter in the following way:

...
@Component({...})
export class HeartbeatComponent{
  @Input() public heartbeatStream: Subject<boolean>;
  public ngOnInit() {
    setInterval(() => this.heartbeatStream.next(true, 500);
  }
}

@Component({
  template: `<heartbeat heartbeatStream="heartbeatSubject"></heartbeat>`
  ...
})
export class ParentComponent {
  private heartbeatSubject = new Subject<boolean>();
  public ngOnInit() {
    this.heartbeatSubject.subscribe(v => console.log(v));
  }
}

When using observables, we can take advantage of the rich set of available operators already provided by RxJs. If you like working with callbacks, there is nothing wrong in the custom events from an architectural point of view.

Using Data Services

At some point, there needs to be a component that actually handles the business functionality triggered by a user action. Consider an AddCommentComponent that implements adding a new component to a blog post. The comment needs to be validated and stored on the server. That sounds like a good task for a CommentService. Thus we would inject the service into the component and when the user submits a new comment, we would invoke an appropriate service method.

Data Flow in Angular 2 Systems

The following diagram presents a high-level view on the data flow architecture in the entire system. The data flows from the services into the components and from parent components to their descendants (using Observables). The data is encoded using our model/domain objects. The actions that modify the state are implemented as methods on appropriate services.
Angular 2 data flow

Alternative Approach

One of the hottest topics in frontend architecture is the idea of uni-directional data flow. It has been popularized by the flux design pattern and Redux library. The idea is that if anyone (e.g. a component) wants to modify the state, it has to send an action to a dispatcher that then applies the action onto the state (that is kept within stores). The views listen for changes from the stores. Redux provides a nice and efficient implementation of the stores. The key idea is that views do not modify the state directly.

The architecture that we describe, defines a similar uni-directional flow. The state is maintained by the services. We do not say how the services should keep the state, you could use Redux under the hood. The services expose a strongly-typed API to modify and access the data. In our approach, to modify the state, a component invokes a service method (instead of submitting an action). The services expose the data using observables to which components (and other services) can subscribe to get the changed data.

We believe that fine-grained services have an architectural advantage over one global state tree. We do not need to inject the store for the entire application to all components. The components have higher cohesion and can be much easier extracted to an enterprise-wide component library.

Wrap-up

There are multiple ways of implementing a data flow in your application. It is important to have a clear view, which technique should be used in which situation and to use them in your system in a consistent way. The model described in this post works well for us and we hope you will find it useful too.

 

comments: 0