Angular — Why we should avoid using public methods to design components

There are many ways to communicate between components. One of the most frequently used approaches is via public method calls. Many developers with a strong classic web development background (such as JQuery) like using this approach to design components. However, it is in fact, not a good practice in the world of Angular.

Issue 1: It cannot utilize the Angular change management

One of the most powerful features of Angular is the auto management of states. When the states in the component are updated, Angular helps to reflect them to the template automatically through change detection.

@Component({
template: `{{value}}`
})
export class TestComponent {
value = 1;
}

For example, when value in class is updated, Angular will auto-update the value in the template. However, we won’t be able to utilize this feature when using components that rely on public methods. Here I would like to explain with an example:

@Component({
selector: 'custom-button',
...
})
export class CustomButtonComponent {
activated = false;
public setActivated(value: boolean) {
this.activated = value;
}
}

For this kind of components, we will have to keep a reference in order to access its public methods.

@Component({
template: `<custom-button #customButton></custom-button>`
})
export class PageComponent {
@ViewChild('customButton') customButton: CustomButtonComponent;
doSomething() {
this.customButton.setActivated(true);
}
}

Now assume we have another component <filter> that has an activeFilter attribute. We want the button to be activated when activeFilter is not empty.

@Component({
template: `
<filter
[activeFilter]="activeFilter"
(activeFilterChange)="onActiveFilterChange($event)"></filter>
<custom-button #customButton></custom-button>
`
})
export class PageComponent {
@ViewChild('customButton') customButton: CustomButtonComponent;
activeFilter = {};

onActiveFilterChange(activeFilter) {
this.activeFilter = activeFilter;
this.customButton.setActivated(activeFilter.length > 0);
}
}

The major problem is that we have to manage the state change ourselves. In this case, we have to call setActivated() manually whenever we know the state is changed. It means we have to find out all the trigger points of state change and put the setActivated() there. This will be a nightmare for bigger components that have more complicated business logic.

By redesigning the component with the binding approach, we can make the code much cleaner:

@Component({
selector: 'custom-button',
...
})
export class CustomButtonComponent {
@Input() activated = false;
}

Now, we can do:

@Component({
template: `
<filter [(activeFilter)]="activeFilter"></filter>
<custom-button
[activated]="activeFilter.length > 0"></custom-button>
`
})
export class PageComponent {
activeFilter = {};
}

The mutation of state can be chained through different components with the help of the Angular state management, which will help to reduce a lot of redundant codes. Most importantly, we no longer need to care about which part of the code may trigger data change. Angular can help us to ensure that whenever activeFilter is updated, the change will be reflected to the template. This highly reduce the complexity and maintenance overhead.

Issue 2: It is not friendly for component inheritance

The next issue is about component re-usability. Yet, it is not related to the component itself, but its parent that using it. Again, I would like to explain with an example:

@Component({
template: `
<div>{{data | json}}</div>
<custom-button #customButton></custom-button>
`
})
export class PageComponent implements OnInit {
@ViewChild('customButton') customButton: CustomButtonComponent;
data = [];
constructor(
private testService: TestService
) {}
ngOnInit() {
this.testService.getData()
.then(data => {
this.customButton.setActivated(data.length > 0);
this.data = data;
})
}
}

Assume we have a TestService that helps to retrieve data. We want the button to be activated when the retrieved data is not empty. The problem here is that the parent must maintain a element reference in order to call the public methods. It creates a strong coupling between the template and the class, which causes a lot of trouble for other developers who are trying to reuse this component through inheritance.

@Component({
template: `
<div>{{data | json}}</div>
<div>Hi, I am a special page without button.</div>
`
})
export class SpecialPageComponent extends PageComponent {
}

When developers want to reuse these codes through inheritance, they must ensure the existence of the #customButton element even if they don’t need it. The SpecialPageComponent above will just throw a null error.

We can remove this constraint by utilizing binding:

@Component({
template: `
<div>{{data | json}}</div>
<custom-button [activated]="data.length > 0"></custom-button>
`
})
export class PageComponent implements OnInit {
data = [];
constructor(
private testService: TestService
) {}
ngOnInit() {
this.testService.getData()
.then(data => {
this.data = data;
})
}
}

Now the logic is stick with the template without any coupling with the class. Developers can reuse the code much more flexibly without the need to compromise the template design.

Conclusion

Public methods are convenient to use, but they also cause other problems. Ideally, we should avoid using public methods when designing component, especially for assignments that can be easily replaced with bindings. For non-assignment public methods, we should try to avoid them in a higher level, such as utilizing content projection to avoid cross-component logic.

On a side note, here is another article I have written previously about other approaches of communication between components.

Thanks for reading! Hope you find this article helpful. Any comments would be highly appreciated. :D

Web developer from Hong Kong. Most interested in Angular and Vue. Currently working on a Nuxt.js + NestJS project.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store