Building Reusable Angular Components: Best Practices

--

Components are the basis of modern SPAs (Angular, Vue, React). They are the simplest building blocks of SP applications. A SPA app is a tree of components, it starts with the root component, going up spreading into multiple branches. A Component contains the View to be displayed, a Controller that contains the display logic and the Model that tells the component how data is to be displayed. The Components are composable, they build up an app by stacking the components on each other like LEGO blocks.

Sharing Components

Before we continue with the main topic of this article — “how to write reusable Angular components”, let’s take a quick look at a great tool that actually lets you share and collaborate on individual reusable components.

Bit helps you and your team develop, share and collaborate on individual components. Each component gets encapsulated with all its dependencies and setup and pushed into a private/public collection on Bit’s cloud where it can easily be discovered by others. Bit transforms each component into a quasi-package — quasi-repository. Easily collaborate and share. Easily install in your projects. Repositories no longer limit modularity and reusability.

Build Shareable Components

Let’s see a scenario where re-usable components come in handy to help us in our project. Developer A is working on a project, a music player that lists music and developer B is working on a music-sharing project. Now, A’s project would require to list the music in the player’s storage, so the player can select a piece of music to play the music. Also, B’s project would require the players’ music files so the user can select a music file he wants to share.

It comes that A has a very nice UI for listing music files. Then, B comes across it and decides that the UI would be great for his music-sharing app. B hits up A and says

B: “Yo, A just came across your music app, I like the UI” A: “Thanks Bee” B: “I’m building a music-sharing app and would like to use your UI” A: “No p”

Now if A’s app is not reusable, he will find it hard to share the UI interface to B. Let’s say A’s app looks this:

@Component({
selector: 'playmusic',
template: `
<div>
<div>
<h2>Your Music</h2>
</div>
<div class="music-list" *ngFor="let music of musicFiles">
<img src="music.image" />
<h3>{{music.name}}</h3>
<h3>{{music.artist}}</h3>
<h3>{{music.duration}}</h3>
<button (click)="playMusic(music)">Play</button>
<hr />
</div>
</div>
`
})
export class PlayMusic implements OnInit {
constructor(private musicService: MusicService) {}
ngOnInit() {
this.musicService.loadAllMusic().subscribe((data) => this.musicFiles = data.music)
}
playMusic(music) {
//...
this.musicService.playMusic(music)
// ...
}
}

See this component loads music files from wherever A wants and stores the array of music returned in the musicFilesproperty. The component iterates through the music array and displays the music files. Pressing the Play button sends the current music to be played to the MusicService#playMusic() method.

Many things are wrong with this component. It has multiple concerns:

  • UI presentation.
  • Business logic: Loading music files from Service and playing music.
  • Injecting service.

With this, A cannot share this component to B. Why? Because B cannot use it. Even if B uses it he has to edit some of the codes to adapt to his context which will be a lot of work for him.

To code is not the issue, to write code well is the issue. With that SOLID patterns were devised, which enables us to write reusable, maintainable and testable code. The 24 Design Patterns was published by Erich Gamma which presents to us design patterns on how to write clean and performant code.

See A’s code does not follow a pattern as such the code is very un-testable, un-reusable and very hard to maintain. For A to make his code good, he needs to follow one of the many patterns. One such pattern is to split your components into “Container” and “Presentational”

Container Components

These are “Smart” components that are responsible for loading the data to be rendered from an external service and passing it to Dumb components. As the name implies “Container”, they contain or are composed of other components, which comprises of “Dumb” and “Smart”.

Container components are smart in the sense that they are not dependent on the parent component for data to render, it gets it data from an injected Service or Store/Effects if using Redux/NgRx.

As they generate their data, they contain no presentational content, they merely pass the data to Presentational components, it is their job to know how to display the data. If a Container component has any presentational content, it is failing the SRP in SOLID, it has multiple concerns and should be extracted.

Presentational Components

These are often called “Dumb” components because they are dependent on their parent component to supply them data to display. They only deal with the display logic, they inject no Service or perform any side-effect action. They receive data through the @Input() bindings and communicate data to the parent components via @Output() binding.

Presentational components are like pure functions they produce an output based on the input given. Their execution affects no external state which makes them predictable and easy to optimize. Presentational components depend on the input to know what to render, we can make the Presentational component highly performant by memoizing the inputs using OnPush, Angular would not re-render the component if its value does not change.

A has to make his code reusable, to do that he has to follow the pattern:

  • Identify multiple concerns in a component.
  • Identify and isolate pieces that can be reused in a new component
  • Make the component as dumb as possible.

Identify multiple concerns in a component

A has to identify the multiple concerns in the PlayMusic component.

@Component({
selector: 'playmusic',
template: `
<div>
<div>
<h2>Your Music</h2>
</div>
<div class="music-list" *ngFor="let music of musicFiles">
<img src="music.image" />
<h3>{{music.name}}</h3>
<h3>{{music.artist}}</h3>
<h3>{{music.duration}}</h3>
<button (click)="playMusic(music)">Play</button>
<hr />
</div>
</div>
`
})
export class PlayMusic implements OnInit {
constructor(private musicService: MusicService) {}
ngOnInit() {
this.musicService.loadAllMusic().subscribe((data) => this.musicFiles = data.music)
}
playMusic(music) {
//...
this.musicService.playMusic(music)
// ...
}
}

The PlayMusic component injects Service, Displays the music lists, Business logic.

The Service MusicService handles the music logic of A’s music app, B’s music sharing app might not need the MusicService, his app’s business logic might be different from A’s. Besides, he only needs the UI (the style of display). The Component injects MusicService, it implements the OnInit lifecycle hook This is where it subscribes to the observable returned byMusicService#loadAllMusic.

Business logic and Services cannot be shared because projects have their business logic. B might load music, but he might load it from a different URL from A if, at all A loads from a network, A might be reading from storage. Do you see? so many unpredictabilities. Both projects deal on music but their mode of operation is different that is the reason Container components cannot be shared. Shareable components are re-usable.

Identify and isolate pieces that can be reused in a new component

Now, A need to isolate the pieces that can be reused in another component. In the above section, we identified that the Component has:

  • Business logic
  • UI presentation

A have to create a new Component maybe call it MusicList and move the UI presentation to the Component.

@Component({
selector: 'playmusic',
template: `
<div>
<div>
<h2>Your Music</h2>
<musiclist [musicFiles]="musicFiles" (playMusic)="plMusic($target)"></musiclist>
</div>
</div>
`
})
export class PlayMusic implements OnInit {
constructor(private musicService: MusicService) {}
ngOnInit() {
this.musicService.loadAllMusic().subscribe(data => this.musicFiles = data.music)
}
// ...
plMusic(target) {
// ...
}
}
@Component({
selector: 'musiclist',
template: `
<div class="music-list" *ngFor="let music of musicFiles">
<img src="music.image" />
<h3>{{music.name}}</h3>
<h3>{{music.artist}}</h3>
<h3>{{music.duration}}</h3>
<button (click)="playMusic.emit(music)">Play</button>
<hr />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MusicList {
@Input() musicFiles;
@Output() playMusic = new EventEmitter();
}

See the concerns have been separated and delegated to its component. The UI presentation was split from the PlayMusiccomponent and is now being handled by MusicList component.

See the data rendered by MusicList component is passed to it by the parent component, MusicList is a dumb component. Now, since its output is determined by the input, it can be optimized to be highly performant, that's why we added a CD strategy to be OnPush. The component will be re-rendered when the input bindings change.

The UI is now abstracted to MusicList component, A can share the component to B. B would have no problem integrating the component to his project. How would there be no problem? The component does not have any business logic, the component gets what it renders via the musicFiles binding and emits an event to the parent via playMusic. All that B needs to do is to fetch the music files from his business logic and feed the array to the MusicList:

@Component({
selector: 'musicshare',
template: `
<div>
<div>
<h2>Music Sharing App</h2>
</div>
<musiclist [musicFiles]="allMusic"></musiclist>
<footer></footer>
</div>
`
})
export class MusicShare implements OnInit {
constructor(private musicShareService: MusicShareService) {}
ngOnInit() {
this.musicShareService.getAllMusic().subscribe(data => this.allMusic = data.allMusic)
}
// ...
shareMusic(music) {
// ...
}
}

See how B easily plugged the MusicList component into his project. His MusicShare component loads music files using the MusicShareService and assigns it to the allMusic property. In the template he just feeds the allMusic array to the musicFiles of MusicList component, the component just renders the music files in the beautiful UI he (B) likes.

The MusicList component is pure, it doesn't affect any part of B's app likewise in other devs apps. It just renders what it is given if in any need of anything (play music or share music) it communicates it to the parent (here MusicShare) component, which is responsible for the logic(business logic) whether on how to play or share the music.

The MusicList component is only concerned with display logic, any other logic is emitted to the parent component. It cannot because different users will use the component for different reasons. Some might use it for:

  • Playing music (Music Player like Boomplayer)
  • Share music (Music sharing app like Spotify)
  • Edit a music (Music Cutting app like Music Cutter in Android)
  • Do all three above

The parent component gets the music and acts on it with the business logic of the app. The MusicList might decide to display the music files either in Row or Grid, display the music files in alphabetical order or any other orders the user wants: Recently played, time, size, date created.

All these are all display logic which is concerned only to the MusicList component. If A sees that devs like his component he might add all the above options.

Make the component as dumb as possible

It is a best practice to make components Dumb whenever possible. Whenever you create a new component always think of ways you can make the component dumb if not possible try isolating components out of the component and make them dumb.

Making dumb components makes our app highly performant. For example The A’s previous PlayMusic component:

@Component({
selector: 'playmusic',
template: `
<div>
<div>
<h2>Your Music</h2>
</div>
<div class="music-list" *ngFor="let music of musicFiles">
<img src="music.image" />
<h3>{{music.name}}</h3>
<h3>{{music.artist}}</h3>
<h3>{{music.duration}}</h3>
<button (click)="playMusic(music)">Play</button>
<hr />
</div>
</div>
`
})
export class PlayMusic implements OnInit {
constructor(private musicService: MusicService) {}
ngOnInit() {
this.musicService.loadAllMusic().subscribe((data) => this.musicFiles = data.music)
}
playMusic(music) {
//...
this.musicService.playMusic(music)
// ...
}
}

We cannot optimize the component. Why? It will always trigger CD on itself, there will be no way to optimize this component because of its side-effects (the HTTP request). If the array of music is very huge there will be a noticeable drag on the app when it tries to load them or reshuffle them or perform any operation on them, though you might solve the problem using lazy loading. We have to make the app update when the array is referentially changed. We create a dumb component that will handle the array and receive the music array through input bindings, as the dumb component can be optimized we will assign it an OnPush CD strategy so that it only re-renders when the input has changed. The PlayMusic component can continuously re-render but because it does not serious work the re-rendering won't have an impact on the app performance.

So we see making a higher ratio of components in our app dumb boosts our app performance. There don’ts in our dumb component: Don’t mutate the inputs, if it’s a must, emit it to the parent using @Output(); Don't side-effect.

Conclusion

We saw the story of how A wants to share a component to B but because of multiple concerns as a result not following best practices was unable to share the component to B. A has to refactor his code to separate concerns following the SRP in SOLID. We went through with A to make his component re-usable and was finally able to share it to B.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me

Thanks !!!

Related Stories

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕