Pure components

The idea of a "pure" component is that its whole state is represented by its inputs, where all inputs are immutable. This is effectively a stateless component, but additionally, all the inputs are immutable.

I like to call such components "pure" because their behavior can be compared to the concept of pure functions in functional programming. A pure function is a function which has the following properties:

  • It does not rely on any state outside of the function scope
  • It always behave the same if input parameters don't change
  • It never changes any state outside the function scope (side-effect)

With pure components, we have a simple guarantee. A pure component will never change without its input parameters being changed. We can ignore a component and its subcomponents in change detection until one of the component inputs changes. Sticking to this idea about components gives us several advantages.

It's very easy to reason about pure components and their behavior can be predicted very easily. Let's look at a simple illustration of a component tree where we use pure components:

Pure components

A component tree with immutable components

If we have the guarantee that each component in our tree has a stable state until an immutable input property changes, we can safely ignore change detection that would usually be triggered by Angular. The only way that such a component could change is if an input of the component changes. Let's say that there's an event that causes the A root component to change the input binding value of the B component, which will change the value of a binding on the E component. This event, and the resulting procedure, would mark a certain path in our component tree to be checked by change detection:

Pure components

Figure that shows a marked path for change detection (in black) with "pure" components.

Although the state of the root component changed, which also changed input properties of the subcomponents on two levels, we only need to be concerned about a given path when thinking about possible changes in the system. Pure components give us the promise that they will not change if their inputs will not. Immutability plays a big role here. Imagine that you're binding a mutable object to the component, B, and the A component would change a property of this object. As we use object references and mutable objects, the property would also be changed for the B component. However, there's no way for the B component to notice this change, as we can't track who knows about our object within the component tree. Basically, we'd need to go back to regular dirty checking of the whole tree again.

By knowing that all our components are pure and that their inputs are immutable, we can tell Angular to disable change detection until an input property value changes. This makes our component tree very efficient, and Angular can optimize change detection effectively. When thinking about large component trees, this can make the difference between a stunningly-fast application and a slow one.

The change detection of Angular is very flexible, and each component gets its own change detector. We can configure the change detection of a component by specifying the changeDetection property of the component decorator.

Using ChangeDetectionStrategy, we can choose from a list of strategies that apply for the change detection of our component. In order to tell Angular that our component should only be checked if an immutable input was changed, we can use the OnPush strategy, which is designed exactly for this purpose.

Let's take a look at the different configuration possibilities of component change-detection strategies and some possible use cases:

Change-detection strategy

Description

CheckAlways

This strategy tells Angular to check this component during every change-detection cycle, and this is obviously the most expensive strategy. This is the only strategy that guarantees that a component gets checked for changes on every possible application state change. If we're not working with stateless or immutable components, or we are using an inconsistent data flow within our application, this is still the most reliable change-detection method. Change detection will be executed on every browser event that runs within the zone of this component.

Detached

This strategy tells Angular to completely detach a component subtree from change detection. This strategy can be used to create a manual change-detection mechanism.

OnPush

This strategy tells Angular that a given component subtree will only change under one of the following conditions:

  • One of the input properties changes where changes need to be immutable
  • An event binding within the component subtree is receiving an event

Default

This strategy simply evaluates to CheckAlways

Purifying our task list

In the previous topic, we changed our main application component to use RxJS Observables in order to get notified about the data changes in our data store.

We also looked into the basics of using immutable data structures and that Angular can be configured to assume component changes only occur when component input changes ("pure" components). As we'd like to get the performance benefits that result from this optimization, let's refactor our task list component to make use of this.

In the previous chapter, we built our TaskList component and directly placed the task data in the component. We then refactored our code so that we could place the task data into a service and use injection to obtain data.

Now, we're rebuilding our TaskList component to make it "pure" and only dependent on its input properties. As we'll make sure that the data flowing into the component is always immutable, we can use the OnPush change-detection strategy on our refactored component. This will certainly give our task list a performance boost.

Equally important as performance, are the structural benefits that we get from using pure components. A "pure" component does not change any data directly because it's not allowed to modify application state. Instead, it uses output properties to emit events on changed data. This allows our parent component to react to these events and perform the necessary steps to handle the changes. As a result of this, the parent component will possibly change the input properties of the pure component. This will trigger change detection and effectively change the state of the pure component.

What might sound a bit overcomplicated at first is actually an immense benefit to the structure of our application. This allows us to reason about our component with high confidence. The unidirectional data flow as well as stateless nature makes it easy to understand, examine, and test our components. Also, the loose nature of inputs and outputs makes our component extremely portable. We can decide on a parent component, what data we'd like to run into our component, and how we'd like to handle changes.

Let's take a look at our TaskList component and how we change it to conform to our concept of "pure" components:

import {Component, ViewEncapsulation, Input, Output, EventEmitter, ChangeDetectionStrategy} from '@angular/core';
import template from './task-list.html!text';
import {Task} from './task/task';
import {EnterTask} from './enter-task/enter-task';
import {Toggle} from '../ui/toggle/toggle';

@Component({
  selector: 'ngc-task-list',
  host: {
    class: 'task-list'
  },
  template,
  encapsulation: ViewEncapsulation.None,
  directives: [Task, EnterTask, Toggle],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TaskList {
  @Input() tasks;
  // Event emitter for emitting an event once the task list has 
  // been changed
  @Output() tasksUpdated = new EventEmitter();

  constructor() {
    this.taskFilterList = ['all', 'open', 'done'];
    this.selectedTaskFilter = 'all';
  }

  ngOnChanges(changes) {
    if (changes.tasks) {
      this.taskFilterChange(this.selectedTaskFilter);
    }
  }

  taskFilterChange(filter) {
    this.selectedTaskFilter = filter;
    this.filteredTasks = this.tasks ? this.tasks.filter((task) => {
        if (filter === 'all') {
          return true;
        } else if (filter === 'open') {
          return !task.done;
        } else {
          return task.done;
        }
      }) : [];
  }

  // Function to add a new task
  addTask(title) {
    const tasks = this.tasks.slice();
    tasks.push({ created: +new Date(), title, done: null });
    this.tasksUpdated.next(tasks);
  }
}

All the operations in our task list component are now immutable. We never directly modify our task's data that was passed in as input, but rather we create new task data arrays to perform mutable operations.

From what we've learned from the previous section, this effectively makes our component a "pure" component. This component itself is only relying on its input and makes our component very easy to reason about.

You've probably noticed that we've also configured the change-detection strategy of our component. As we have a "pure" component now, we can configure our change-detection strategy accordingly to save some performance:

@Component({
  selector: 'ngc-task-list',
  …
  changeDetection: ChangeDetectionStrategy.OnPush
})

As we're rendering a Task component for each data record in our task list, we should also check what we can change there in order to round this out.

Let's look at the changes in our Task component:

import {Component, Input, Output, EventEmitter, ViewEncapsulation, HostBinding, ChangeDetectionStrategy} from '@angular/core';
import template from './task.html!text';
import {Checkbox} from '../../ui/checkbox/checkbox';

@Component({
  selector: 'ngc-task',
  host: {
    class: 'task'
  },
  template,
  encapsulation: ViewEncapsulation.None,
  directives: [Checkbox],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Task {
  @Input() task;
  // We are using an output to notify our parent about updates
  @Output() taskUpdated = new EventEmitter();

  @HostBinding('class.task--done')
  get done() {
    return this.task && this.task.done;
  }

  // We use this function to update the checked state of our task
  markDone(checked) {
    this.taskUpdated.next({
      title: this.task.title,
      done: checked ? +new Date() : null
    });
  }
}

We also use the OnPush strategy for our Task component, and we can do this because we also have a pure component here. This component only depends on its inputs. Both inputs expect native values (a String for title and a Boolean for done), which actually makes them immutable by nature. Changes on the task will be communicated using the taskUpdated output property.

Now, this is a good time to think about where to place our task list in the application. As we're writing a task management system that gives our users the ability to manage tasks within projects, we need to have a container that will encapsulate the concerns of projects. We create a new Project component, on the path lib/project/project.js, which will display project details and renders the TaskList component as a subcomponent:

import {Component, ViewEncapsulation, Input, Output, EventEmitter, ChangeDetectionStrategy} from '@angular/core';
import template from './project.html!text';
import {TaskList} from '../task-list/task-list';

@Component({
  selector: 'ngc-project',
  host: {
    class: 'project'
  },
  template,
  encapsulation: ViewEncapsulation.None,
  directives: [TaskList],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Project {
  @Input() title;
  @Input() description;
  @Input() tasks;
  @Output() projectUpdated = new EventEmitter();

  // This function should be called if the task list of the 
  // project was updated
  updateTasks(tasks) {
    this.projectUpdated.next({
      title: this.title,
      description: this.description,
      tasks
    });
  } 
}

Again, we make the state of this component dependent only on its immutable inputs, and we use the OnPush strategy in order to get the positive performance implications of using a pure component. It's also important to note that the updateTasks function acts as some sort of a delegate from our TaskList component. When we update a task inside the TaskList component, we catch the event in the project template and call the updateTasks function with the new updated task list. From here, we're just emitting the updated project data with the new task list further up in the component tree.

Let's also take a look at the template of our Project component quickly to understand the wiring behind this component:

<div class="project__l-header">
  <h2 class="project__title">{{title}}</h2>
  <p>{{description}}</p>
</div>
<ngc-task-list [tasks]="tasks"
               (tasksUpdated)="updateTasks($event)">
</ngc-task-list>

The binding logic in the template tells us how the whole data flow with our purified components work. While the Project component itself receives the list of tasks as input, it directly forwards this data to the tasks input of the TaskList component. If the TaskList component fires a tasksUpdated event, we're calling the updateTasks method on the Project component, which in fact just emits a projectUpdated event again.

Recap

The refactoring of our task list is now completed, and we applied our knowledge about immutable components and observable data structures to gain some performance wins in this structure. There won't be unnecessary dirty checking on our Task component any more because we switched to the OnPush change-detection strategy.

We have also reduced the complexity of the TaskList and Task components a lot, and it's now far easier to reason about these components and their state.

A further benefit of this refactoring is the great encapsulation level that we achieved using immutable inputs. Our TaskList component is not relying on any task container as a project. We can also pass it a list of tasks across all the projects, and it can still work as expected.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset