Stateful Data Binding: Mastering Two-Way Communication and State Management in Angular Applications

Handle complex scenarios involving non-nested components or sibling components with two-way communication and state management.

Kenneth Paul Tengco
Bits and Pieces

--

Photo by Farzad on Unsplash

Introduction

In the world of Angular development, establishing efficient communication between components while managing their state can be a formidable challenge. Fortunately, Angular provides a powerful tool called data binding that enables seamless data transfer and synchronization between different parts of your application. But what happens when you need to go beyond simple component interactions? How do you handle complex scenarios involving non-nested components or sibling components? That’s where mastering two-way communication and state management becomes crucial.

Problems

  • Complex Component Interactions: In applications with multiple components that need to communicate and share data, establishing seamless and efficient communication channels can be challenging. Stateful data binding simplifies this process, enabling components to exchange data bi-directionally.
  • Real-Time Updates: When it’s necessary for changes in one component to be instantly reflected in another component, stateful data binding allows for real-time updates. This is particularly useful in scenarios where immediate synchronization of data is crucial for a smooth user experience.
  • Interactive User Interfaces: Building interactive user interfaces often requires capturing user input, responding to events, and dynamically updating the UI. Stateful data binding simplifies this process by enabling components to react to user interactions and update the UI accordingly.
  • Simplified Data Flow: By establishing a direct connection between components through stateful data binding, developers can simplify the flow of data and reduce the need for complex event handling or callback mechanisms.
  • Code Maintainability: With stateful data binding, code becomes more readable and maintainable. The bidirectional nature of data flow reduces the need for manual updates and synchronization, leading to cleaner and more modular codebases.

Solution

Through clear explanations, practical examples, and best practices, we will unveil the secrets of establishing seamless two-way communication between components. We will delve into the intricacies of managing state alongside data binding, enabling you to build robust and scalable Angular applications. By the end of this guide, you’ll be equipped with the necessary tools to unlock the full potential of data binding and state management, empowering you to create dynamic and interactive experiences in your Angular projects.

💡 Pro Tip: If you need to use any of the logic used in the following lines for your other projects, consider extracting them from your codebase into their own components and sharing them across multiple projects with the help of a tool like Bit. Bit comes with a component development environment for Angular that provides proper tools, configurations and runtime environment for your components, providing versioning, testing, and dependency management features to simplify the process.

Learn more:

Structure

How to Implement

In this comprehensive guide, we will dive deep into the world of stateful data binding in Angular. We will explore techniques that not only facilitate bidirectional data flow but also allow you to manage component state effectively. Whether you’re a beginner or an experienced Angular developer looking to enhance your skills, this article will equip you with the knowledge and practical strategies needed to overcome the challenges of data binding and state management in Angular applications.

Step 1: Building our Application’s Module

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { CommonModule } from "@angular/common";

import { AppComponent } from "./app.component";

import { UserContactComponent } from "src/component/user-contact";
import { UserContactViewComponent } from "src/component/user-contact-view";
import { UserDocumentComponent } from "src/component/user-document";
import { UserDocumentViewComponent } from "src/component/user-document-view";
import { UserInfoComponent } from "src/component/user-info";
import { UserInfoCardComponent } from "src/component/user-info-card";

@NgModule({
declarations: [
AppComponent,
UserContactComponent,
UserContactViewComponent,
UserDocumentComponent,
UserDocumentViewComponent,
UserInfoComponent,
UserInfoCardComponent
],
imports: [
CommonModule,
BrowserModule
],
bootstrap: [
AppComponent
]
})
export class AppModule { }

In our application, we will create components that are associated with a user. We will divide these components into six parts: user’s contact, document, contact view, document view, info, and info card components.

Step 2: Constructing the Layout for our App (Bootstrap) Component

import { Component } from "@angular/core";

import { UserContactService } from "src/shared/service/user-contact";
import { UserDocumentService } from "src/shared/service/user-document";
import { UserInfoService } from "src/shared/service/user-info";

@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
viewProviders: [
UserContactService,
UserDocumentService,
UserInfoService
]
})
export class AppComponent { }

Here is our simple app component. It is important to note the presence of view providers, which are necessary for facilitating the sharing of service provider instances between the view and its child components. These view providers are crucial for the proper functioning of the six components we will be building. Without them, the functionality of these components would be affected.

<app-user-info-card></app-user-info-card>

<div class="flex">
<app-user-info class="flex-1"></app-user-info>
<app-user-document class="flex-1"></app-user-document>
<app-user-contact class="flex-1"></app-user-contact>
</div>

Provided here is the template for our App component, comprising four distinct components, each with its own set of functionalities and behaviors. These components serve the purpose of displaying data bindings derived from the shared service providers. Additionally, we have two other reusable components nested within ‘app-user-info’, ‘app-user-document’, and ‘app-user-contact’, each with its own unique functionalities and behaviors as well.

Step 3: Constructing the Shared Service Providers in Our Application

import { Injectable } from "@angular/core";

import { UserInfoModel } from "src/shared/model/user-info";

@Injectable()
export class UserInfoService {

public get firstName(): string {
return this.info?.firstName;
}

public get lastName(): string {
return this.info?.lastName;
}

public get birthdate(): string {
return this.info?.birthdate;
}

private info!: UserInfoModel;

public updateInfo(contact: UserInfoModel): void {
this.info = contact;
}
}

User Info Service: This service is responsible for displaying user information such as first name, last name, and birthdate.

import { Injectable } from "@angular/core";

import { UserContactModel } from "src/shared/model/user-contact";

@Injectable()
export class UserContactService {

private readonly _list: UserContactModel[] = [];

public get list(): UserContactModel[] {
return this._list;
}

public addContact(contact: UserContactModel): void {
this._list.push(contact);
}
}

User Contact Service: This service handles the management and display of user contacts.

import { Injectable } from "@angular/core";

import { UserDocumentModel } from "src/shared/model/user-document";

@Injectable()
export class UserDocumentService {

private readonly _list: UserDocumentModel[] = [];

public get list(): UserDocumentModel[] {
return this._list;
}

public addDocument(contact: UserDocumentModel): void {
this._list.push(contact);
}
}

User Document Service: This service manages and displays user documents or valid IDs.​

Step 4: Creating the User Info Card Component

import { Component } from "@angular/core";

import { UserContactService } from "src/shared/service/user-contact";
import { UserDocumentService } from "src/shared/service/user-document";
import { UserInfoService } from "src/shared/service/user-info";

@Component({
selector: "app-user-info-card",
templateUrl: "./user-info-card.component.html",
styleUrls: ["./user-info-card.component.scss"]
})
export class UserInfoCardComponent {

public get contactCount(): number {
return this.userContactService.list.length;
}

public get documentCount(): number {
return this.userDocumentService.list.length;
}

public get firstName(): string {
return this.userInfoService.firstName;
}

public get lastName(): string {
return this.userInfoService.lastName;
}

public get birthdate(): string {
return this.userInfoService.birthdate;
}

constructor(
private readonly userContactService: UserContactService,
private readonly userDocumentService: UserDocumentService,
private readonly userInfoService: UserInfoService
) { }
}

User Info Card Component: This component provides a quick view of the user. It utilizes three service providers to display the user’s name, birthday, and the number of contacts and documents they have.​

<div class="card">
<h3>User Info Card Component</h3>

<table class="table">
<tbody>
<tr>
<td>First Name</td>
<td>Last Name</td>
<td>Birthdate</td>
</tr>

<tr>
<td>{{firstName}}</td>
<td>{{lastName}}</td>
<td class="red">{{birthdate}}</td>
</tr>

<tr>
<td># of Contact Type</td>
<td># of Document Type</td>
<td></td>
</tr>

<tr>
<td class="blue">{{contactCount}}</td>
<td class="green">{{documentCount}}</td>
<td></td>
</tr>
</tbody>
</table>
</div>

Presented here is the template for our User Info Card component, which serves the purpose of showcasing data bindings obtained from the shared service providers utilized in our application.

Step 5: Creating the User Document Component

import { Component } from "@angular/core";

import { UserDocumentModel } from "src/shared/model/user-document";
import { UserDocumentService } from "src/shared/service/user-document";

@Component({
selector: "app-user-document",
templateUrl: "./user-document.component.html",
styleUrls: ["./user-document.component.scss"]
})
export class UserDocumentComponent {

private get list(): UserDocumentModel[] {
return this.userDocumentService.list;
}

constructor(
private readonly userDocumentService: UserDocumentService
) { }

public add(): void {
const documentCount = this.list.length + 1;

const document = new UserDocumentModel(
`passport-${documentCount}`,
`xyz-${documentCount}`
);

this.userDocumentService.addDocument(document);
}
}

User Document Component: This component utilizes the User Document Service to update only the list of documents or valid IDs associated with the user.​

<div class="card">
<div class="title-action">
<h3>User Document Component</h3>

<button type="button" (click)="add()">Add</button>
</div>

<app-user-document-view></app-user-document-view>
</div>

Displayed here is the template for our User Document component, which is in charge of updating the document list. Take note of the ‘app-user-document-view’ component, which is utilized to render the updated list of documents. Additionally, this ‘app-user-document-view’ will be reused in the ‘app-user-info’ component, ensuring synchronous updates between the two.

Step 6: Creating the User Document View Component

import { Component } from "@angular/core";

import { UserDocumentModel } from "src/shared/model/user-document";
import { UserDocumentService } from "src/shared/service/user-document";

@Component({
selector: "app-user-document-view",
templateUrl: "./user-document-view.component.html",
styleUrls: ["./user-document-view.component.scss"]
})
export class UserDocumentViewComponent {

public get list(): UserDocumentModel[] {
return this.userDocumentService.list;
}

public get noDocument(): boolean {
return this.list.length === 0;
}

public readonly cols: string[] = ["Document Type", "Description"];

constructor(
private readonly userDocumentService: UserDocumentService
) { }
}

User Document View Component: This component utilizes the User Document Service to retrieve only the list of documents or valid IDs associated with the user.​

<div class="card">
<h3>User Document View Component (Child Component)</h3>

<table class="table">
<thead>
<tr>
<ng-container *ngFor="let col of cols">
<th>{{col}}</th>
</ng-container>
</tr>
</thead>

<tbody>
<ng-container *ngFor="let document of list">
<tr>
<td>{{document.documentType}}</td>
<td>{{document.description}}</td>
</tr>
</ng-container>

<ng-container *ngIf="noDocument">
<tr>
<td [attr.colspan]="cols.length" class="text-center">
No record
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>

Displayed here is the template for the User Document View component, responsible for rendering the list of documents in a table format.

Step 7: Creating the User Contact Component

import { Component } from "@angular/core";

import { UserContactModel } from "src/shared/model/user-contact";
import { UserContactService } from "src/shared/service/user-contact";

@Component({
selector: "app-user-contact",
templateUrl: "./user-contact.component.html",
styleUrls: ["./user-contact.component.scss"]
})
export class UserContactComponent {

private get list(): UserContactModel[] {
return this.userContactService.list;
}

constructor(
private readonly userContactService: UserContactService
) { }

public add(): void {
const contactCount = this.list.length + 1;

const contact = new UserContactModel(
`12345-${contactCount}`,
`@gmail.com">abc.${contactCount}@gmail.com`,
`Contact ${contactCount}`
);

this.userContactService.addContact(contact);
}
}

User Contact Component: This component utilizes the User Contact Service to update only the list of contacts associated with the user.

<div class="card">
<div class="title-action">
<h3>User Contact Component</h3>

<button type="button" (click)="add()">Add</button>
</div>

<app-user-contact-view></app-user-contact-view>
</div>

Displayed here is the template for our User Contact component, which is in charge of updating the contact list. Take note of the ‘app-user-contact-view’ component, which is utilized to render the updated list of contacts. Additionally, this ‘app-user-contact-view’ will be reused in the ‘app-user-info’ component, ensuring synchronous updates between the two.

Step 8: Creating the User Contact View Component

import { Component } from "@angular/core";

import { UserContactModel } from "src/shared/model/user-contact";
import { UserContactService } from "src/shared/service/user-contact";

@Component({
selector: "app-user-contact-view",
templateUrl: "./user-contact-view.component.html",
styleUrls: ["./user-contact-view.component.scss"]
})
export class UserContactViewComponent {

public get list(): UserContactModel[] {
return this.userContactService.list;
}

public get noContact(): boolean {
return this.list.length === 0;
}

public readonly cols: string[] = ["Description", "Phone", "Email"];

constructor(
private readonly userContactService: UserContactService
) { }
}

User Contact View Component: This component uses the User Contact Service to retrieve only the list of available contacts for the user.​

<div class="card">
<h3>User Contact View Component (Child Component)</h3>

<table class="table">
<thead>
<tr>
<ng-container *ngFor="let col of cols">
<th>{{col}}</th>
</ng-container>
</tr>
</thead>

<tbody>
<ng-container *ngFor="let contact of list">
<tr>
<td>{{contact.description}}</td>
<td>{{contact.phone}}</td>
<td>{{contact.email}}</td>
</tr>
</ng-container>

<ng-container *ngIf="noContact">
<tr>
<td [attr.colspan]="cols.length" class="text-center">
No record
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>

Displayed here is the template for the User Contact View component, responsible for rendering the list of documents in a table format.

Step 9: Creating the User Info Component

import { Component, OnInit } from "@angular/core";

import { UserInfoModel } from "src/shared/model/user-info";
import { UserInfoService } from "src/shared/service/user-info";

@Component({
selector: "app-user-info",
templateUrl: "./user-info.component.html",
styleUrls: ["./user-info.component.scss"]
})
export class UserInfoComponent implements OnInit {

public get firstName(): string {
return this.userInfoService.firstName;
}

public get lastName(): string {
return this.userInfoService.lastName;
}

public get birthdate(): string {
return this.userInfoService.birthdate;
}

private updateCounter: number = 0;

constructor(
private readonly userInfoService: UserInfoService
) { }

public ngOnInit(): void {
this.userInfoService.updateInfo(new UserInfoModel(
"bob",
"doe",
"1970-01-01"
));
}

public update(): void {
this.updateCounter++;

this.userInfoService.updateInfo(new UserInfoModel(
"bob",
"doe",
`${1970 + this.updateCounter}-01-01`
));
}
}

User Info Component: This component provides a comprehensive view of the user’s information, contacts, and documents. It primarily utilizes the User Info Service to display the user’s first name, last name, and birthdate. Additionally, it contains child components responsible for rendering the complete view of the user’s documents and contacts.​

<div class="card">
<div class="title-action">
<h3>User Info Component</h3>

<button type="button" (click)="update()">update</button>
</div>

<table class="table">
<tbody>
<tr>
<td>First Name</td>
<td>{{firstName}}</td>
</tr>

<tr>
<td>Last Name</td>
<td>{{lastName}}</td>
</tr>

<tr>
<td>Birthdate</td>
<td>{{birthdate}}</td>
</tr>
</tbody>
</table>

<app-user-document-view class="user-meta"></app-user-document-view>

<app-user-contact-view class="user-meta"></app-user-contact-view>
</div>

Presented here is the template for our User Info component, designed to showcase comprehensive user data, including a full view of documents and contacts. The ‘app-user-document-view’ and ‘app-user-contact-view’ components are synchronized with updates to user documents and contacts, ensuring consistent and up-to-date information.

Handling State Management

import { Component, AfterViewChecked } from "@angular/core";

import { UserContactService } from "src/shared/service/user-contact";
import { UserDocumentService } from "src/shared/service/user-document";
import { UserInfoService } from "src/shared/service/user-info";

@Component({
selector: "app-user-info-card",
templateUrl: "./user-info-card.component.html",
styleUrls: ["./user-info-card.component.scss"]
})
export class UserInfoCardComponent implements AfterViewChecked {

public get contactCount(): number {
return this.userContactService.list.length;
}

public get documentCount(): number {
return this.userDocumentService.list.length;
}

public get firstName(): string {
return this.userInfoService.firstName;
}

public get lastName(): string {
return this.userInfoService.lastName;
}

public get birthdate(): string {
return this.userInfoService.birthdate;
}

private contactListCount!: number;
private documentListCount!: number;

constructor(
private readonly userContactService: UserContactService,
private readonly userDocumentService: UserDocumentService,
private readonly userInfoService: UserInfoService
) { }

public ngAfterViewChecked(): void {
this.contactCountChanged();
this.documentCountChanged();
}

private contactCountChanged(): void {
if (this.contactListCount === undefined) {
this.contactListCount = this.contactCount;
return;
}

if (this.contactListCount !== this.contactCount) {
this.contactListCount = this.contactCount;

console.log('UserInfoCardComponent: contact count changed detected, we can do now some API call or whatever process');
}
}

private documentCountChanged(): void {
if (this.documentListCount === undefined) {
this.documentListCount = this.documentCount;
return;
}

if (this.documentListCount !== this.documentCount) {
this.documentListCount = this.documentCount;

console.log('UserInfoCardComponent: document count changed detected, we can do now some API call or whatever process');
}
}
}

In this example, we will utilize the User Info Card component to showcase how simple state management can be handled. Pay attention to the implementation of the AfterViewChecked lifecycle method, which is triggered during change detection checks. Additionally, we have introduced new private methods in our component, specifically contactCountChanged and documentCountChanged. These methods compare the count flags of the contact list and document list with the count of the original source after the change detection process. This behavior is reminiscent of how computed libraries update their values in Angular v16 when changes take place. It provides an opportunity to perform additional actions or logic when a value has changed.

Demo

Pros

  • Seamless Communication: Stateful data binding enables effortless two-way communication between components, allowing for real-time updates and synchronized data flow.
  • Improved User Experience: By instantly reflecting changes in the user interface, stateful data binding enhances interactivity and provides a smoother user experience.
  • Simplified Development: With data binding, developers can write less code to establish communication channels, resulting in cleaner and more maintainable codebases.
  • Flexibility and Scalability: Stateful data binding offers flexibility in handling complex scenarios, such as non-nested component interactions, and provides a scalable foundation for application growth.

Cons

  • Complexity: As the application grows and the number of components increases, managing stateful data binding can become complex and challenging to maintain.
  • Performance Impact: Excessive or poorly optimized data bindings can lead to performance issues, affecting the overall responsiveness of the application.
  • Potential Data Inconsistencies: In some cases, bidirectional data flow can introduce complexities in managing and ensuring data consistency, requiring careful attention and planning.

Conclusion

In conclusion, we have explored the world of stateful data binding in Angular applications. By mastering two-way communication and state management techniques, we can establish seamless connections between components and ensure consistent state synchronization. Armed with these skills, we are now equipped to build dynamic and interactive Angular applications with ease and maintainability.

The beauty of stateful data binding in Angular lies in its simplicity. Unlike relying on external libraries like Signal, RxJS, or NgRx, stateful data binding leverages Angular’s built-in features. By utilizing Angular’s data binding syntax and directives, developers can establish seamless communication and state management without the need for additional dependencies. This simplicity streamlines the development process and enhances code maintainability and scalability. This is the power of stateful data binding in Angular and enjoy the elegance of its built-in capabilities for efficient and interactive application development.

Considerations for Advanced Scenarios

It’s important to evaluate the specific requirements and complexity of your application when considering the use of external libraries. While stateful data binding is often sufficient for many use cases, these libraries can provide additional capabilities and abstractions for handling specific challenges and scenarios in more advanced or complex Angular applications.

  • Complex Asynchronous Operations: If your application involves complex asynchronous operations, such as making HTTP requests, handling WebSocket connections, or dealing with streams of data, libraries like RxJS can provide powerful abstractions and operators to handle these scenarios more efficiently. RxJS’s reactive programming model and extensive operator library make it suitable for managing complex async workflows.
  • Advanced State Management: As your application grows in complexity, managing state across multiple components can become challenging. External state management libraries like NgRx provide a structured and centralized approach to state management using concepts like actions, reducers, and selectors. They can be particularly useful for large-scale applications with complex state requirements, offering benefits such as improved organization, time-travel debugging, and better separation of concerns.
  • Cross-Component Communication: If you are rendering hundreds of components and have a need for extensive cross-component communication or complex event-driven architectures, libraries like Signal can provide a powerful and flexible messaging system. Signal-based libraries facilitate decoupled communication between components, allowing for more fine-grained control and modularity.
  • Advanced Data Flow Control: In certain scenarios, you may require more advanced data flow control mechanisms beyond what stateful data binding offers. External libraries can provide additional tools, such as observables or event streams, that enable fine-grained control over data propagation and transformation.

Keep practicing and applying these concepts to create robust and scalable solutions. Happy coding!

Thanks for reading! Don’t forget to subscribe if you want more updates! 😉

You may also help by buying me a coffee ☕️ for small support.

Build Angular Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--