Take a look at these tips for creating clean code. Some of them are applicable across development, and some are specifc to Angular developers.
Now that Angular 19 is available, it is time to write cleaner code! We must use both regular clean coding techniques and those specific to Angular.
Stop using *ngIf
, and start using the control flow syntax.
<!-- Nope! -->
<div *ngIf="isTrue; else DoThis">
<p>Why did I have to create an extra div?</p>
</div>
<ng-template #DoThis>
<p>BTW, you must use ng-template, didn't you know!?</p>
</ng-template>
<!-- Use this! -->
@if (isTrue) {
<p>Much better! You can do a @Switch as well!</p>
}
@else if (somethingElse) {
<p>Before you had to nest the ifs! Yuck!</p>
}
@else {
<p>Obviously inspired by Svelte!</p>
}
This includes @defer
, @if
, @else
, @for
and @switch
. See Angular 17 Dev Experience: A Practical Guide. The code is cleaner, and the bundle size is much smaller!
Injection tokens are freaking amazing. This is how all frameworks should work and eliminate the Context API.
export const FIREBASE_AUTH = new InjectionToken<Auth | null>(
'firebase-auth',
{
providedIn: 'root',
factory() {
const platformID = inject(PLATFORM_ID);
if (isPlatformBrowser(platformID)) {
return inject(Auth);
}
return null;
}
}
);
This is much easier to use than a class with a service. Functions can also be imported like normal variables in components. Hopefully, classes will be eventually replaced with functions, even in components.
const auth = inject(FIREBASE_AUTH);
See my Analog Todo App with Firebase example.
If you’re a pro-Angular user, you use the Angular CLI to generate new components easily. For small quick components, you should always use inline styles and inline templates to save time and have all your code in one place.
npm generate component component_name --inline-template --inline-style
And you should be using the shorthand version:
npm g c component_name -t -s
This makes editing your components extremely easy and provides better readability.
import { Component } from '@angular/core';
@Component({
selector: 'app-temp',
standalone: true,
imports: [],
template: `
<header>
<h1>Some Component!</h1>
<div>Some stuff about some other stuff.</div>
</header>
`,
styles: ``,
})
export class TempComponent {}
These days styles
may not even be necessary if you’re using Tailwind.
📝 I didn’t mention standalone
components, which is now the default option. However, I’m hoping you’re already using them!
You can now remove ZoneJS from your application saving 13KB of space!
bootstrapApplication(App, {
providers: [
provideExperimentalZonelessChangeDetection()
]
});
The future of Angular is zoneless! If you’re using Signals correctly, your entire Angular Change Detection process will be cleaner!
📝 You must also remove the Zone.js polyfills!
RxJS is not optional yet, but it will be eventually. You should not use RxJS in Angular unless you need to handle asynchronous events with race conditions or coordinate complex data flow. See Why Didn’t the Angular Team just use RxJS instead of Signals?
// No more need for Behavior Subjects
private _user = new BehaviorSubject<UserType>({
loading: true,
data: null
});
user = this._user.asObservable();
// Just use Signals
const user = signal<{
loading: boolean,
data: userData | null
}>({
loading: true,
data: null
});
Angular enthusiasts love to fetch data in a component. This made sense with RxJS, refreshing component parameters, and now with Signals. If fetching data, you may want to handle race conditions with abort controllers, toggle loading state and display errors. However, if you’re loading data that needs to be resolved on the server, you should be using resolvers. Processing fetch results on the server is faster, but more importantly, you probably need the fetched data for SEO purposes in your meta tags and schema.
// todos.resolver.ts
import { inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, ResolveFn } from '@angular/router';
import { map } from 'rxjs';
export const todosResolver: ResolveFn<Promise<any>> = async (route, state) => {
const todoId = route.paramMap.get('id');
if (!todoId) {
throw new Error('Todo ID is missing in the route!');
}
// Fetch the todo from the API
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`);
if (!response.ok) {
throw new Error('Failed to fetch the todo');
}
const todo = await response.json();
return todo;
};
export const injectResolver = <T>(name: string) =>
inject(ActivatedRoute).data.pipe<T>(map(r => r[name]));
export const injectResolverSignal = <T>(name: string) =>
toSignal<T>(injectResolver(name));
// app.routes.ts
export const routes: Routes = [
{ path: 'todos/:id', component: TodosComponent, resolve: { data: todosResolver } },
...
];
Here is a resolver for fetching todos
. They can be easily used in your component.
todo = injectResolverSignal<Todo>('data');
The signal will refresh when the page route refreshes.
While there are many design principles in SOLID, the popularity shift to functional programming has made one stand out. These principles hold for all JavaScript frameworks.
Single Responsibility Principle (SRP) states that “[t]here should never be more than one reason for a class to change.” In other words, every class should have only one responsibility.
While SOLID initially talked about object-oriented programming (OOP), functional programming in JavaScript shares common features.
export const LOGIN = new InjectionToken(
'LOGIN',
{
providedIn: 'root',
factory() {
const auth = inject(FIREBASE_AUTH);
return () => {
if (auth) {
signInWithPopup(
auth,
new GoogleAuthProvider()
);
return;
}
throw "Can't run Auth on Server";
};
}
}
);
This injection token has one purpose: to sign in a user with Firebase.
“Keep it Simple, Stupid” works outside of programming, but is also important when writing components.
if
statement can do.A good example of this would be optimizing for the future. If you get many users, you will have money to optimize later.
Don’t repeat yourself.
📝 I would also argue it is OK to separate a function only used once. Long functions are equally as hard to keep up with.
You aren’t gonna need it!
👓 As you can see, all of these core coding principles overlap.
Refractor your if
statements to return early instead of using complex if / else
statements. This made my coding extremely simple!
// Before
function processUserInput(input: string | null): string {
if (input !== null) {
if (input.trim() !== "") {
if (input.length > 5) {
return `Valid input: ${input}`;
} else {
return "Input is too short";
}
} else {
return "Input is empty";
}
} else {
return "Input is null";
}
}
// After
function processUserInput(input: string | null): string {
if (input === null) return "Input is null";
if (input.trim() === "") return "Input is empty";
if (input.length <= 5) return "Input is too short";
return `Valid input: ${input}`;
}
⁉️ Which is easier to read?
Comments have their place, but they are not necessary if name your variables better.
function calculateTotalPrice(cartItems: CartItem[]): number {
let totalPrice: number = 0;
for (const cartItem of cartItems) {
totalPrice += cartItem.price * cartItem.quantity;
}
return totalPrice;
}
This function is easy to understand because the variables are clearly named. Don’t use x
or i
, for example, when it is unclear what a function is doing.
While Angular continues to evolve over time, writing cleaner code will become easier. However, clean coding principles will never die.
Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.