Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jason Dean and Mark Drew
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jason Dean ( @JasonPDean ) Mark Drew ( @markdrew )

On The Irrational Demonization Of Two-Way Data-Binding In Angular

By on

The other day, I was listening to a JavaScript podcast on which a guest of the show pointed to two-way data-binding as one of the biggest "problems" in Angular. This is not a new thought - it's something that I hear time-and-time again, especially from people in the React world. This demonization of two-way data-binding is completely irrational. And, unfortunately, if it's said by enough "thought leaders", it can become quite detrimental to the mental model of newer developers. So, I just wanted to come out and say that I absolutely love the two-way data-binding in Angular. In fact, I think it's one of the features that gives Angular its power. And, it's a feature that I use every single day with great success.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Now, to be fair, you can absolutely create bad data flows that leverage two-way data-biding in Angular. I've done this. But, this problem is usually the result of poor data-ownership boundaries - not the mechanism of two-day data-binding itself. At the end of the day, you always have the power to write crap code in any context. The process of learning is the very process of writing slightly less crappy code every day.

Before we explore the power of two-way data-binding, let's try to define what it is. This is probably not a perfect definition; but, I think of two-way data-binding as the mechanism that synchronizes data in a bidirectional way across a touch-point.

For example, one of the most common uses of two-way data-binding is in a Form control. With a Form control, changes in your View Model are automatically pushed into your input element. And, changes in your input element are also automatically pushed back into your View Model. This two-way data-binding keeps your View Model and the Input state of your template synchronized.

Now, as powerful as two-way data-binding is, it's a feature that you have to opt-into in Angular. Angular doesn't force you to use two-way data-bindings - it simply provides support for it if you happen to want to make your life easier. And, in fact, we can even develop custom components in Angular that provide support for both one-way (unidirectional) and two-way data-binding. This way, not even consumers of our custom components have to use two-way data-binding if they don't want to.

To explore the powerful, opt-in nature of two-way data-binding in Angular, let's create a custom component for "tagging". This component will accept an array of tags (strings); and, allow the user to enter new tags and remove existing tags.

It will accept one input binding:

  • tags: string[]

And, it will expose several output bindings:

  • add: EventEmitter<string>
  • remove: EventEmitter<number>
  • tagsChange: EventEmitter<string[]>

The first two outputs, "add" and "remove", are there to facilitate one-way (or "unidirectional") data flow. As the tags component emits these events, the consuming context can figure out how to use these events in order to mutate the "tags" input binding that gets pumped back into the component.

The last output, "tagsChange", is there to facilitate two-way data-binding. The name of this output is important. In Angular, when you have an input named "value" (where "value" could be anything) and an output named "valueChange", Angular provides the "box of bananas" template syntax for two-way data-binding:

[(value)]="view.value"

This syntax is essentially a short-hand for the following two-way data flow:

[value]="view.value" (valueChange)="view.value = $event"

Here, you can see that the emitted value in the event, "valueChange", is being immediately applied back to the View Model, which is, in turn, being pumped back into the input binding, "value".

With that said, let's look at the TagsComponent. One thing to notice about this component is that it exposes one-way and two-way data bindings; but, it also consumes a two-way data flow internally with NgModel. As part of the component implementation, NgModel is being used to synchronize the embedded input element with the internal state of the TagsComponent view model.

// Import the core angular services.
import { ChangeDetectionStrategy } from "@angular/core";
import { Component } from "@angular/core";
import { EventEmitter } from "@angular/core";
import { OnChanges } from "@angular/core";
import { SimpleChanges } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// This component supports both ONE-WAY and TWO-WAY data-binding. The TWO-WAY data-
// bindings is facilitated by the "tags" and "tagsChange" output events. These events
// allow for the "box of bananas" template syntax.
@Component({
	selector: "bn-tags",
	inputs: [ "tags" ],
	outputs: [
		"tagAddEvents: add",
		"tagRemoveEvents: remove",
		"tagsChangeEvents: tagsChange"
	],
	changeDetection: ChangeDetectionStrategy.OnPush,
	styleUrls: [ "./tags.component.less" ],
	template:
	`
		<label (click)="input.focus()">
			<span *ngFor="let tag of tags ; let index = index" class="tag">
				<span class="tag__name">
					{{ tag }}
				</span>
				<a (click)="removeTagAtIndex( index )" class="tag__delete">
					⌫
				</a>
			</span>

			<!-- NOTE: Input uses NgModel for component-local TWO-WAY DATA-BINDING. -->
			<input
				#input
				type="text"
				name="newTagName"
				[(ngModel)]="newTagName"
				(keydown.Enter)="processNewTag( $event )"
			/>
		</label>
	`
})
export class TagsComponent implements OnChanges {

	public newTagName: string;
	public tagAddEvents: EventEmitter<string>;
	public tagRemoveEvents: EventEmitter<number>;
	public tags: string[];
	public tagsChangeEvents: EventEmitter<string[]>;

	// I initialize the tags component.
	constructor() {

		this.newTagName = "";
		this.tagAddEvents = new EventEmitter();
		this.tagRemoveEvents = new EventEmitter();
		this.tags = [];
		this.tagsChangeEvents = new EventEmitter();

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called after input bindings have been changed.
	public ngOnChanges( changes: SimpleChanges ) : void {

		// This component requires "tags" to exist. If it doesn't exist, the component
		// functionality will be fundamentally broken.
		if ( ! this.tags ) {

			throw( new Error( "Required input [tags] not provided." ) );

		}

	}


	// I process the new tag name.
	public processNewTag( event: KeyboardEvent ) : void {

		// Since this may be inside of a Form, we want to prevent the default behavior
		// of the key-event so as to not accidentally submit the parent form.
		event.preventDefault();

		if ( this.newTagName ) {

			// Emit new tag name for one-way data flow.
			this.tagAddEvents.emit( this.newTagName );

			// Emit NEW ARRAY with applied change for TWO-WAY data-binding.
			this.tagsChangeEvents.emit( this.tags.concat( this.newTagName ) );

			// Reset the form field.
			this.newTagName = "";

		}

	}


	// I process the removal of the tag at the given index.
	public removeTagAtIndex( index: number ) : void {

		// Emit new tag index for one-way data flow.
		this.tagRemoveEvents.emit( index );

		// Emit NEW ARRAY with applied change for TWO-WAY data-binding.
		this.tagsChangeEvents.emit([
			...this.tags.slice( 0, index ),
			...this.tags.slice( index + 1 )
		]);

	}

}

As you can see, the TagsComponent accepts a "tags" input collection. But, it never mutates this collection directly - it only emits events that indicate the mutation intent of the user. It's up to the calling context to figure out how to parle said intent into an actual change in the View Model.

When it comes to the embedded input element, the TagsComponnet uses NgModel to automatically synchronize the internal state with the component template state. This works seamlessly because the TagsComponet completely owns the implementation details of this data workflow. It's just another example of how two-way data-binding makes life easier.

Now that we see how our tags component is emitting values that facilitate both one-way and two-way data-binding, let's look at how this component can be consumed. In the root component of the Angular application, I've setup two instances of the TagsComponnet: one that uses two-way data-binding; and, one that uses one-way data-binding in conjunction with several public methods provided by the root component:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<h3>
			Using Two-Way Data Binding
		</h3>

		<bn-tags [(tags)]="tagsA"></bn-tags>

		<h3>
			Using One-Way Data Flow
		</h3>

		<bn-tags
			[tags]="tagsB"
			(add)="addTagToB( $event )"
			(remove)="removeFromB( $event )">
		</bn-tags>
	`
})
export class AppComponent {

	public tagsA: string[];
	public tagsB: string[];

	// I initialize the app component.
	constructor() {

		this.tagsA = [ "awesome", "cool" ];
		this.tagsB = [ "bad", "lazy" ];

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I handle the "add" event being emitted from the tags component for TagsB.
	public addTagToB( newTagName: string ) : void {

		this.tagsB = this.tagsB.concat( newTagName );

	}


	// I handle the "remove" event being emitted from the tags component for TagsB.
	public removeFromB( index: number ) : void {

		this.tagsB = [
			...this.tagsB.slice( 0, index ),
			...this.tagsB.slice( index + 1 )
		];

	}

}

As you can see, the first "bn-tags" element that uses the "box of bananas" two-way data-binding syntax requires no additional effort. The "tagsChange" event emitted by the TagsComponnet is implicitly saved to the "tagsA" collection and piped back into the TagsComponnet using the "tags" input binding. Easy peasy lemon-squeezey.

The second "bn-tags" element, on the other hand, uses a one-way data-binding workflow and therefore has to provide event handlers for the "add" and "remove" events emitted by the TagsComponent. In this case, these event handlers are doing exactly what the two-way data-binding is doing; but, they don't have to. For example, the one-way data flow handlers could implement logic that skips or transforms certain "add" events. Hooking into the "decision making" is exactly when you want to use a one-way data-binding workflow.

ASIDE: In this case, I'm binding the two-way data worflow directly to the View Model value. But, in practice, I tend to isolate two-way data-binding connections behind a "form" hash. This way, I create separation between the source-inspiration of the data and the mutation of that data.

Now that we see how the AppComponent is consuming the TagsComponent using both two-way and one-way data-binding, let's try running the application. If I open up this Angular app in the browser and add a few tags to each instance, we get the following output:

The irrational demonization of two-way data-binding in Angular is unfortunate.

As you can see, both data-binding approaches - one-way and two-way - work perfectly. In the two-way data-binding approach, changes emitted by the TagsComponent are seamlessly piped back into our View Model. And, in the one-way data-binding approach, changes emitted by the TagsComponent are explicitly managed by the event-handlers in our AppComponent. The one-way data binding requires more code; but, it offers more flexibility. Two-way data-binding, on the other hand, requires no additional code; but, requires buy-in for the implicit synchronization of data.

In this post, not only did we create a custom component in Angular that exposed one-way and two-way data-binding hooks, we also used NgModel to consume two-way data-binding within the internal implementation of our custom component. Hopefully, this exploration has helped shed some light on the exciting benefits provided by two-way data-binding. And, will help you better evaluate the constitution of people who dismiss two-way data-bindings out-of-hand.

Want to use code from this post? Check out the license.

Reader Comments

8 Comments

Great post. A while ago I was completely sold on the idea that one-way data flow would be a silver bullet for maintaining state properly. This is a good example that the right tool for the right job should be considered when you are constructing components. More code certainly means more potential for mistakes.

Cheers

1 Comments

Thanks for this article :)
I would summarize it as "With great power comes great responsibility"

In my opinion this is another topic labeled as "problem" because it brings great power and does not set strict boundaries which let less experienced devs to make more mistakes from architectural point of view.

Actually recently it's very common approach to set up frameworks and solutions which eases creating very complex applications by setting strict rules and boundaries (eg. typescript, redux).
I'm not saying it's bad idea as it really gives opportunity to speed up development. But as you mentioned because of that some really great tools and mechanisms are sometimes unnecessarily demonized :)

PS: There is small typo there:

(...) this problem is usually the result of poor data-ownership boundaries - not the mechanism of two-day data-binding itself.

(which made my day as... two-day binding could be actually really problematic ????????)

15,640 Comments

@Malgosia,

Oh man, you have no idea how many times I typed "two-day data-binding". When I re-read the article (pre-publish), like every-other instance said that. It's like some textual spoonerism :D

That said, I agree with what you are saying. Essentially, the more you program, the more you understand where you can and can't / shouldn't use certain paradigms. And, we stumble a lot as we are learning. But, I think that's "OK". It's part of the process of becoming a better programming.

If the framework in which we are working can "help you" make fewer mistakes, that's awesome. But, at the same time, I like when the framework also let's you step outside the box when you want to / need to. Take something like Event Binding. Both Angular and React provide mechanisms for adding event-handlers through an abstraction such that the events are automatically unbound when elements are destroyed / unmounted. However, both frameworks also let you grab raw DOM references and add your manage your own event handlers should you need to.

Can you imagine if Angular or React said you can never touch a raw DOM element just because you have the possibility of forgetting to unbind it and cause a memory leak and unexpected behavior? It would be crazy :D

Another good example is immutability. Immutability has a lot of benefits in terms of change-detection. But, this does not in any way mean that mutability is bad. I use both in different situations. Can you imagine how much boilerplate you would need if all form data had to be immutable? Ugg, what a one-way data-binding nightmare. Of course, using two-way data-binding in a form does not mean that two-way data-binding makes sense everywhere.

Anyway, I've pontificated enough. Long-story short, whatever makes the code easier to reason about and easier to maintain is the "right" approach.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel