Angular 2: one-way data flow with Redux

Horrified! That seemed to be the collective reaction when the Angular team announced two-way data binding would not be directly present in Angular2. Since then, the team have worked hard to reassure the community.

However, one-way data flows are attracting ever more devotees. This pattern has been popularised by React / Redux, and is epitomised by Elm. One-way binding, coupled with a single (immutable) source of state truth, is increasingly seen as a route to faster, hot reloadable, testable code, while enabling stunning (time-travelling) debug tools.

What would a one-way, single state pattern look like in Angular2? This post seeks to emulate the basic patterns in the first four examples from The Elm Architecture.

The basic pattern

Throughout these examples, we will keep state in a parent component. The parent will inform the children of the state they need to render, and the children will send action events back to the parent to cause state updates. After each update, the parent will cause the children to re-render with new values. All the code is in Typescript and can be found here, and see the different branches corresponding to each example and an updated to Angular2-beta of the final Redux version is here.

Example 1: A counter

To start we will implement a single, simple counting component that shows its currently value and which can be increased or decreased, as shown in the image. A Counter

Let’s start with the ‘View’ template, which simply attaches a click listener to each button using the new Angular syntax.

<button (click)="action('dec')">Decrement</button>
<span>{{count}}</span>
<button (click)="action('inc')">Increment</button>

Now let’s look at the component’s Javascript.

import { Component, View, Input, Output, EventEmitter } from 'angular2/angular2';

@Component({ selector: 'counter' })
@View({ templateUrl: 'app/components/counter/counter.html' })
export class CounterComponent {
  @Input() count;
  @Output() updater = new EventEmitter();

  constructor() { }

  action(val) {
    let delta: Number = (val === 'inc') ? 1 : -1;
    this.updater.next(this.count + delta);
  }
};

There are several things to note here:

The CounterComponent is instantiated by the parent App component.

import { Component, View } from 'angular2/angular2';
import { CounterComponent } from '../counter/counter-component';

@Component({ selector: 'app' })
@View({
  template: `<counter [count]="model" (updater)="pupdate($event)"></counter>`,
  directives: [CounterComponent],
})
export class AppComponent {
  model: number;

  constructor() {
    this.model = 0;
  }

  pupdate(newVal) {
    this.model = newVal;
  }
}

Here we see:

Example 2 : Two Counters

How can we extend the pattern above to handle two counters? Let’s introduce a Counters component that will simply be instantiated by App.

Counters provides this View:

<counter [count]="model.top" (updater)="pupdate('top')($event)"></counter>
<counter [count]="model.bot" (updater)="pupdate('bot')($event)"></counter>

And uses this controller:

@Component({ selector: 'counters' })
@View({
  templateUrl: 'app/components/counters/counters.html',
  directives: [CounterComponent]
})
export class CountersComponent {
  model: Object;

  constructor() {
    this.model = {
      "top" : 10,
      "bot" : 20
    }
  }

  pupdate(modelName) {
    return ( newVal => this.model[modelName] = newVal);
  }  
};

And that’s it:

Example 3 : Many Counters

OK, two was good but what about a flexible number? Again, we won’t need to touch Counter, but we will need some additional tools for its parent, Counters.

export class CountersComponent {
  model: Array<Number>;

  constructor() {
    this.model = [];
  }

  addCounter() {
    this.model.push(0);
  }

This time our model has become an array, and we need a method to add elements to the model. pupdate does not need changing but note that it’s modelName parameter now will be the index in the model Array. (We also bring in the NgFor Directive, which we need in the rewritten View.)

<button (click)="addCounter()">Add counter</button>
<counter
	\*ng-for="#val of model; #i=index"
	[count]="val"
	(updater)="pupdate(i)($event)">
</counter>

Example 4: Adding an extra action

In example 3 we could only add a Counter. What if we want to remove them too? This time we will need to change Counter a little to add <button (click)="remove()">X</button> to the View and implement the remove method to emit a second type of Event up to the parent:

@Output() remover = new EventEmitter();
remove() {
  this.remover.next('X');
}

Counters needs to provide for this extra event when it instantiates new counter elements:

<counter
	\*ng-for="#val of model; #i=index"
	[count]="val"
	(updater)="pupdate(i)($event)"
	(remover)="premove(i)($event)">
</counter>

And premove works equivalently to addCounter to update the global state:

premove(idx) {
  return (e => this.model.splice(idx, 1));
}

Using Redux & immutable for state management

So far, we have used mutable state, but it straightforward to add in calls to redux by the parent node of our Angular 2 app.As the focus here is on Angular I won’t present all of the Redux and immutable data handling code, but just show my Angular code’s redux calls. You can find the full code in the repo.

export class CountersComponent {
  model: Array<Number>;
  store: any;

  constructor() {
    this.store = makeStore();

    this.store.subscribe(
      () => this.model = this.store.getState().toJS()
    );
  }

  addCounter() {
    this.store.dispatch({ type: ADD_COUNTER });
  }

  premove(idx) {
    return (e => this.store.dispatch({ type: REMOVE_COUNTER, index: idx}));
  }

  pupdate(idx) {
    return (newVal => this.store.dispatch({ type: UPD_COUNTER, index:idx, value:newVal}));
  }
}

Conclusions

We have seen a succession of examples that added functionality while retaining a single source of state truth. Rendering was effectively ‘pure’ and actions were transmitted to the parent to maintain the state. The Angular loop is still quite different from Elm’s but we have achieved some of the core elements on the one-way data binding pattern.