Angular has rapidly grown to become one of the most popular frameworks for building front-end, cross-platform web applications. It gives you a lot of the out-of-the-box features, such as a routing system, a dependency injection framework, forms handling, etc. Angular also enforces you to use both TypeScript and RxJS, since its already part of the Angular ecosystem. This extensive width of features makes Angular a good candidate for large enterprise solutions.
While Angular is an extremely powerful framework with a broad toolkit, it’s hard to master, especially if it’s your first JavaScript framework. In an attempt to reduce complexity, I decided to put together a clean code checklist which covers my personal recommendations for writing clean and production-ready Angular code.
As we become better developers, structuring and organizing code becomes more and more important. We want to write code that improves readability, scalability, maintainability and performance, and minimizes the time spent debugging.
As Martin Golding stated:
You may also like: Angular vs React vs Vue: Which one will be popular in 2020.
Sounds disturbing, but it doesn’t make the message any less true. Programmers are really authors and other developers are their target audience. The time spent attempting to understand someone else’s code takes up large portion of our day to day operations. We should therefore consciously try to improve the quality of our code. It’s essential for creating a successful, maintainable product.
The logical place to start looking for best practices in Angular is the Angular Style Guide. This is an opinionated guide on syntax, conventions, and application structuring. The style guide presents preferred conventions and explains with examples why you should use them.
The Angular CLI is a great tool for creating and working with Angular applications. It increases productivity by taking away the tedious work of creating each file manually. With only a few commands, you’re able to:
You can find more information about the Angular CLI here.
As the application grows in size, it’s important to have a structure in place that allows for easy management and maintenance of your code base. Whichever structure you decide to use, it’s important to be consistent and choose a structure the entire team is happy with.
If you want the refactoring to be as efficient as possible, it’s important to have readable code. Readable code is easier to understand, which makes it easy to debug, maintain and extend.
While adding new files, pay attention to the file-names you decide to use. File names should be consistent and describe the feature by dot separation.
|-- my-feature.component.ts
or
|-- my-service.service.ts
You need to name the variables and functions so the next developer understands them. Be descriptive and use meaningful names — clarity over brevity_._
This will help us avoid writing functions like this:
function div(x, y)) {
const val = x / y;
return val;
}
And hopefully more like this:
function divide(divident, divisor) {
const quotient = divident / divisor;
return quotient;
}
When we write functions to execute some business logic, we should keep them small and clean. Small functions are easier to test and maintain. When you start noticing that your function is getting long and cluttered, it’s probably a good idea to abstract some of the logic to a new one.
This avoids functions like this:
addOrUpdateData(data: Data, status: boolean) {
if (status) {
return this.http.post<Data>(url, data)
.pipe(this.catchHttpErrors());
}
return this.http.put<Data>(`${url}/${data.id}`, data)
.pipe(this.catchHttpErrors());
}
}
And hopefully more like this:
addData(data: Data) {
return this.http.post<Data>(url, data)
.pipe(this.catchHttpErrors());
}
updateData(data: Data) {
return this.http.put<Data>(`${url}/${data.id}`, data)
.pipe(this.catchHttpErrors());
}
It is extremely valuable to know if a piece of code is being used or not. If you let unused code stay, then in the future, it can be hard or almost impossible to be certain if it’s actually used or not. Therefore, you should make it a high priority to remove unused code.
Although there are cases for comments, you should really try to avoid them. You don’t want your comments to compensate for your failure to express the message in your code. Comments should be updated as code is updated, but a better use of time would be to write code that explains itself. Inaccurate comments are worse than no comments at all, as stated by anonymous:
Code never lies, comments do.
This avoids writing code like this:
// check if meal is healthy or not
if (meal.calories < 1000 &&
meal.hasVegetables) {
...
}
And hopefully more like this:
if (meal.isHealthy()) {
...
}
Angular is built around separation of concerns. This is a design-pattern that makes our code easier to maintain and extend, and more reusable and testable. It helps us encapsulate and limit the logic of components to satisfy what the template actually needs, and nothing more. Separation of concerns is the core of writing clean code in Angular, and uses the following rule-set:
|-- modules
| |-- home
| | |-- home.spec|module|component|scss||routing.module|.ts
| |-- about
| | |-- about.spec|module|component|scss|routin.module|.ts
Quick tip! You can also create an API service which handles a lot of the HTTP-related logic for you. I recommend checking out this GitHub-example.
// src/app/shared/components/reusable/resuable.component
...
export class ReusableComponent implements OnInit {
@Input() title: string;
@Input() body: string;
@Output() buttonClick = new EventEmitter();
constructor() { }
ngOnInit() {}
onButtonClick(){
this.buttonClick.emit('Button was clicked');
}
}
--------------------------------------------------------------------
// You can then use this component directly inside the components of
// your choice
// src/app/some/some.component
@Component({
selector: 'app-some',
template: `<app-reusable [title]="'Awesome title!'"
[body]="'Lorem ipsum'"
(buttonClick)="buttonClick($event)>
</app-reusable>`,
})
export class SomeComponent implements OnInit {
constructor() {}
ngOnInit() {}
public buttonClick(e){
console.log(e);
}
}
Quick tip! We’re able to control the HTML contents from the “parent” component by using the _ng-content_
tag. Read more about content projection with _ng-content_
here.
TypeScript is a superset of JavaScript which primary provides static typing, classes and interfaces. Angular is built on TypeScript, and as a result, using TypeScript with Angular becomes a pleasurable experience. It provides us with advanced autocompletion, quick navigation and refactoring. Having such a tool at your disposal shouldn’t be taken for granted.
To get the most out of TypeScript:
// .../burger.model.ts
export interface Burger {
name: string;
calories: number;
}
// .../show-burger.component.ts
this.burger: Burger;
export interface Burger {
...,
bestBefore: string | Date;
}
Quick tip! We should always declare variables and constants with a type definition other than “any”. Strictly typed code is less error-prone.
One way of improving your development experience immensely is by using TSLint. This is a static code analysis tool we use in software development for checking if TypeScript code complies with the coding rules. Having these rules in place will give you an error when you do something wrong.
You can combine TSLint with Prettier. Prettier is an amazing tool that enforces a consistent style by parsing your code and re-printing it, with it’s own rules in place. Having Prettier setup with TSLint gives you a strong foundation for your applications, as you no longer need to maintain your code-style manually. Prettier takes care of code formatting and TSLint handles the rest.
Even with these rules in place, it can be hard to maintain them. You can easily forget to run these commands before pushing your code to production, which leads to a broken result. One way to work around this problem is by using husky. Husky allows us to run custom scripts before the staged files are committed — keeping our production code clean and organized.
RxJS is a library for reactive programming that allows us to work with asynchronous data streams. Angular comes packaged with RxJS, so it’s to our great advantage to make the most of it.
RxJS 5.5 introduced pipeable operators. This pipe-based approach to observable-composition allows us to import on a per-operators basis, rather than importing everything. Pipeable operators does also have tree-shaking advantages and allows us to build own custom operators without extending the Observable.prototype
.
This will help us avoid writing code like this:
...
const name = this.loadEmployees()
.map(employee => employee.name)
.catch(error => Rx.Observable.of(null));
And hopefully more like this:
const name = this.loadEmployees()
.pipe(
map(employee => employee.name),
catchError(error => of(null))
);
Imagine a case where we need to subscribe to multiple streams. It would be a headache to manually map every single property to the corresponding value and manually unsubscribe when the component gets destroyed.
With the async pipe, we subscribe to the stream directly inside our template, without having to store the result in an intermediate property. The subscription will terminate when the component gets destroyed, which makes subscription-handling easier and contributes to cleaner code.
This will help us avoid writing code like this:
@Component({
...
template: `<items [items]="item">`
})
class AppComponent {
items: Item[];
constructor(private itemService: ItemService) {}
ngOnInit() {
this.loadItems()
.pipe(
map(items => this.items = items;
).subscribe();
}
loadItems(): Observable<Item[]> {
return this.itemService.findItems();
}
}
And hopefully more like this:
@Component({
...
template: `<items [items]="items$ | async"></items>`
})
class AppComponent {
items$: Observable<Item[]>;
constructor(private itemService: ItemService) {}
ngOnInit() {
this.items = this.loadItems();
}
loadItems(): Observable<Item[]> {
return this.itemService.findItems();
}
}
While Angular takes care of unsubscribing when using the async pipe, it quickly becomes a mess when we have to do this on our own. Failing to unsubscribe will lead to memory leaks, as the observable stream is left open.
The solution is to compose our subscription with the takeUntil
operator, and use a subject that emits a value when the component gets destroyed. This will complete the observable-chain, causing the stream to unsubscribe.
This help us avoid writing code like this:
this.itemService.findItems()
.pipe(
map((items: Item[]) => items),
).subscribe()
And hopefully more like this:
private unsubscribe$: Subject<void> = new Subject<void>();
...
this.itemService.findItems()
.pipe(
map(value => value.item)
takeUntil(this._destroyed$)
)
.subscribe();
...
public ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
this.unsubscribe$.unsubscribe();
}
There may be situations where you need to consume data from multiple observable streams. In those cases, you should generally try to avoid socalled nested subscriptions. Nested subscriptions becomes hard to understand and may introduce unexpected side effects. We should instead use chainable methods like switchMap
, forkJoin
and combineLatest
to condense our code.
This will help us avoid writing code like this:
this.returnsObservable1(...)
.subscribe(
success => {
this.returnsObservable2(...)
.subscribe(
success => {
this.returnsObservable3(...)
.subscribe(
success => {
...
},
And hopefully more like this:
this.returnsObservable1(...)
.pipe(
flatMap(success => this.returnObservable2(...),
flatMap(success => this.returnObservable3(...)
)
.subscribe(success => {...});
Quick tip! There may be confusion around when to use the appropriate operators when dealing with multiple streams. I recommend checking out this article for some clarity on this topic.
A Subject acts as both an observable and an observer. Because Subjects allows us to emit data into an observable stream, people tend to abuse them. This is especially popular among developers new to RxJS. To determine when it might be a good time to use a Subject, we’re first going to see what subjects are, and what subjects are not.
What subjects are:
next()
method. This is the primarily use of Subjects in RxJS.What subjects are not:
next()
method on a completed Subject.Quick tip! On top of vanilla Subject, there are also a few specialized types of subjects like async subjects, behavior subjects and replay subjects. You can read more about those types of subjects here.
Consider a case where you need to import something far down the application hierarchy. This would lead to an import statement looking something like this:
import 'reusableComponent' from '../../../shared/components/reusable/reusable.component.ts';
Then imagine that you need to change the location this file. You would then have to update the path of every single file which uses that feature. This doesn’t sound very efficient, does it?
We can clean up these imports considerably by using aliases to reference our files, which looks something like this:
import 'reusableComponent' from '@app/shared/components/reusable.component.ts';
To be able to do this, we need to add a baseUrl
and the desired paths
inside our tsconfig.json
file:
{
"compilerOptions": {
...
"baseUrl": "src",
"paths": {
"@app:": ["@app/*"]
}
}
}
The most common use case for imports are component and services from the Core- and Shared module. To avoid writing out the entire path for those features, we’ll create multiple index.ts
-files, which exports those features:
// src/app/shared/components/index.ts
export * from './reusable/reusable.component.ts';
// src/app/shared
export * from '/components';
We can now reference the file import like this:
import 'ReusableComponent' from '@app/shared/';
Quick tip! Be aware when using path aliases to import services or components inside the Shared- or Core module. This may lead to “circular dependencies”. In those cases, you need to write the entire path.
Animation is defined as the transition from an initial state to a final state. It is an integral part of any modern web application. Animation not only helps us create a great UI but it also makes the application interesting and fun to use. A well-structured animation keeps the user engaged with the application and enhances the user experience.
If your using a multiple-module architecture, it’s probably a good idea to lazy load your modules. Only the feature module which is used to display the initial content to the user should be loaded synchronously. The advantage of using lazy loading is that module isn’t loaded before the user actually accesses the route. This helps decrease the overall startup time by only loading the modules we’re currently presenting.
When building large, complex applications that has lots of information coming from and going to the database, and where state is shared across multiple components, you might start considering adding a state management library. By using a state management library, you are able to keep the application state in one single place, which reduces the communication between components and keeps your app more predictable and easier to understand.
Angular is a very powerful framework with a lot of functionality. But if you’re new to the game, it can seem overwhelming with all its different options and whatnot. Hopefully, by following the guidelines outlined here, some of the concepts became more clear to you. Although there are no blueprint for how to write clean code, there’s some key takeaways here:
Write readable code. Focus on writing code that is easy to understand.
Separation of concerns. Encapsulate and limit logic inside your components, services and directives. Each file should only be responsible for a single functionality.
Utilize TypeScript. TypeScript provides advanced autocompletion, navigation and refactoring. Having such a tool at your disposal shouldn’t be taken for granted.
Use TSLint. TSLint checks if TypeScript code complies with the coding rules in place. Combined with Prettier and Husky makes for an excellent workflow.
RxJS in Angular. RxJS comes packaged with Angular, it’s to our great advantage to make the most of it.
Clean up imports with path aliases. We can clean up our imports considerably by using path aliases to reference our files.
Lazy load your modules. Lazy loading helps us decrease the startup time of our app by only loading the modules we’re currently presenting.
State management (if necessary). State management is a great tool to have at your disposal, but should only be used for large, complex applications where multiple components shares the same state.
Thank for reading ! If you enjoyed this post, please share it with others who may enjoy it as well.!
Related Articles
☞ Keeping your JavaScript code clean forever and scalable
☞ Keeping your Vue.js code Clean
☞ 7 tips on writing clean code
#angular #angular-js #javascript #web-development