Data flow Best Practices
When Angular 1.5 introduced the one way binding into components, it enabled what is considered to be a best practice across the web: uni-directional data flow.
Uni-directional Data Flow
What does this even mean when it comes to web applications? If you consider the componentization of web applications in modern frameworks like Vue, Angular, React etc, one common pattern for data flow emerges as being the most maintainable. That is, data flows down the DOM, and events flow up.
This mentality is inline with my thoughts on Dumb vs Smart components video. Your outermost components should be those that know where the data is coming from, fetching it, and dispersing it among its children.
As the child components respond to the data or the user interacts with them, callbacks should be executed or events spawned to notify parents of such changes. The parent is then responsible for determining how to properly react.
Immutability
The main reason for uni-directional data flow is to ensure immutability of data. Children modifying state can have unforeseen consequence when not doing so in isolation. Side effects caused by data mutation in child components is very difficult to track down because of the complexity of modern web applications. The more components on the screen, the more difficult this becomes.
Angular Specific: Bindings
In pre 1.5 angular, the =
binding (two way binding) was used very heavily when passing data between components. It was super cool when first introduced (you mean I can output a label that changes when a user types in the input box!?). However, the manipulation of data fields by children became a problem in complex applications. It takes only one to break an entire application.
Angular 1.5 introduced one way data bindings (<
) that helps alleviate this problem. It's not perfect; for instance, you can modify properties of an object reference and push an element to an array. Creating a copy of the object during the $onChanges
phase can fix that particular problem.
The combination of the one way binding and the expression binding (&
) to bind callbacks is the proper combination to meet the targeted goal of down-flowing data and up-flowing events.
Concrete Example
Let’s say we have an input component. It’s dumb in its requirements (meaning it doesn’t care what the input values are). The initial value is provided by a parent controller, and any changes made to that value are passed to a red/green indicator as to whether or not the input is a number (showing red if it is not a number, green if it is).
The latter component is defined as such:
.component('highlightProblem', {
template: `
<div ng-class="{'is-error': $ctrl.isNotNumber }"></div>
`,
bindings: {
input: '<'
},
controller: class {
get isNotNumber() {
return !/^\d*$/.test(this.input);
}
}
})
Our parent component has a callback for handling changes in the one way example and the initial values set to 42.
.component('parent', {
controller: class {
$onInit() {
this.oneWay = 42;
this.twoWay = 42;
}
onOneWayChange(input) {
if (/^\d*$/.test(input)) {
this.oneWay = input;
} else {
this.oneWay = 42;
}
}
},
template: `<div>
<two-way input="$ctrl.twoWay"></two-way>
<label>Two Way</label>
<highlight-problem input="$ctrl.twoWay"></highlight-problem>
<one-way input="$ctrl.oneWay" on-input-change="$ctrl.onOneWayChange(input)"></one-way>
<label>One Way</label>
<highlight-problem input="$ctrl.oneWay"></highlight-problem>
</div>`
})
Now, we have two identical components, one using a one way data bind and expression binding for callbacks, and another using the two way data binding.
.component('twoWay', {
bindings: {
input: '='
},
template: `<input type="text" ng-model="$ctrl.input">`
})
.component('oneWay', {
bindings: {
input: '<',
onInputChange: '&'
},
template: `<input type="text" ng-model="$ctrl.input" ng-change="$ctrl.onInputChange({input: $ctrl.input})">`
})
There’s a bit more to the oneWay
component due to the extra step of notifying the parent, but now the parent can enforce the "numbers only" concept in its onInputChange
handler. If we wanted to enforce the same capabilities in the two way data bound component, we would have to make it no longer "dumb" to the fact that the parent wants numbers only. We've now moved the complexity of the problem into the lowest child, making it less reusable throughout the application.
If we didn’t want to enforce it inside the input itself, we’d have to enforce it in the highlightProblem
component. In either case, we've now taken generic components and added complexity to them, when it really only needs to exist in the parent
component. The parent
already knows something about the application, and adding that logic at that point doesn't further complicate the application.
To see this in action, you can view this codepen.io