Components are the fundamental building blocks of Angular applications. This article shares some common practices you should implement for more readable code and a more organized workspace. The topics covered in this article include some practices on component roles, naming conventions, and organization techniques.
Single Responsibility Principle (SRP)
Applying SRP to your project makes sure that each class (components, services, pipes, directives, etc.) only focuses on one functionality, such as listing items, taking form input, etc. In order to apply SRP to your application, first you need to determine which components are smart and dummy, and what each of them will do. This principle offers some benefits that include:
- reducing the amount of code per file due to covering less functionality,
- preventing side-effects and bugs,
- reducing the amount of changes you need to make inside the classes due to limited responsibility,
- speeding up the development process overall,
- making the program easier to understand
Let’s consider an example where we have an application for storing recipes. This app consists of a list to choose from, and a section to view, edit, or add new recipes. Let this app have the following components: NavbarComponent and ViewComponent. NavbarComponent is responsible for the website name and navigating to different pages, while ViewComponent is responsible for listing the recipes and adding/editing/deleting recipes with the click of a button, which includes listing the items and taking input via a form. In the current state, ViewComponent has a lot of functionalities to cover.
How to apply SRP?
We should divide ViewComponent into 3 different components, namely:
- RecipesListComponent – on the sidebar to list the recipes,
- RecipeCardComponent – to show details on the right side,
- RecipeFormComponent – to handle user inputs.
Additionally, we can add a RecipeManagementService to handle the CRUD operations on the recipes, so we can have the AppComponent communicate with these components via signals and the rest will operate as presentational components.
Change Detection Strategy
Angular uses change detection strategies to determine when it’s necessary to check a component for any changes. There are 2 strategies: Default and OnPush. Think of Default as the nurse who checks on every patient, and OnPush as the nurse who checks on the patients whose state changes in any way, or upon request.
With Default strategy, the child component is always checked as long as the parent component is checked. With that, when a part of the application is changed, Angular checks all the components in the family tree. This is inefficient, as it leads to unnecessary checks in the other components that weren’t updated. Some common triggers of Default strategy include HTTP requests, timers, and user events.
OnPush, on the other hand, restricts the checking conditions in order to avoid any unnecessary checks. OnPush contains the CheckOnce strategy, which checks a component once, when it’s marked as dirty, and then skips it throughout the runtime. In large applications OnPush shows a significant performance difference – especially on mobile devices. When a child component changes, all its ancestors, one by one, are marked dirty and checked for change detection. For example:
AppComponent
HeaderComponent
ContentComponent
TodoListComponent
TodoComponent
Above, is a component tree hierarchy in a project where all components have OnPush. Let’s assume something changed inside TodoComponent:
Root Component -> LViewFlags.Dirty
|
...
|
ContentComponent -> LViewFlags.Dirty
|
|
TodoListComponent -> LViewFlags.Dirty
|
|
TodoComponent (event triggered here) -> markViewDirty() -> LViewFlags.Dirty
The changes in TodoComponent are picked up by Zone.js. Then it informs the root component. But the changes made in TodoComponent flag it as dirty, and all its ancestors as well. The entire line of components are flagged, so Zone.js can see which components to check further. HeaderComponent remains untouched in this case, because none of its children, or itself, had any changes to be flagged. And because it uses OnPush, it is ignored.
In bigger applications, setting change detection strategy to OnPush reduces the amount of components to be checked in case of an asynchronous change, thus making it more efficient. It’s usually recommended to set change detection to OnPush in all of the components of your application, as it isn’t costly at all and your app will save time. 🙂
For more detailed information and examples, check here and here.
Lazy loading
With lazy loading technique, you can set up the elements to be loaded when necessary, instead of them being loaded at the initialization stage. This practice reduces the initial bundle size of the application, which results in faster initial load. Remember that the deferred elements may take a bit longer to load. The point of lazy loading is to let Angular know the priorities, i.e, what absolutely needs to load initially and what doesn’t. As applications grow, the data usage increases too. So it’s a good practice to apply this to get the app initialized at a reasonable time instead of making everything load at once.
First make sure you lazy-load all your page components in the routes. You can do this by applying the lazy loading methods for router with loadChildren():
{
path: 'heavy',
loadChildren: () => import('./heavy/heavy.module').then(m => m.HeavyModule)
}
In Angular, you can also apply deferred loading within components by using @defer blocks like this:
@defer {
<large-component />
}
All components, services, and directives rendered inside @defer block are split into separate JavaScript files to be loaded only when necessary after the initialization stage. However this only works for standalone components, as non-standalone components will eagerly load regardless. To lazy load a module with @defer you need to create a standalone wrapper component for it:
@Component({
standalone: true,
selector: 'lazy-heavy',
template: '<ng-container *ngComponentOutlet="cmp()"></ng-container>',
imports: [CommonModule],
})
export class LazyHeavyComponent {
cmp = signal<Type<unknown> | null>(null);
async ngOnInit() {
const { HeavyComponent } = await import('./heavy/heavy.component');
this.cmp.set(HeavyComponent);
}
}
Then you’ll be able to use the component wrapper inside the @defer block.
Naming conventions
This section is about naming conventions to improve readability of your code. Angular has its own style guide, which it implements in the project files when you create them using Angular CLI. We’ll firstly cover the Angular Style Guide, and then I’ll mention some naming conventions that you should remember.
- File names:
If a component’s name consists of more than one word, you should set the file name by putting hyphens “-” between each word, followed by file extension, e.g user-profile.ts, user-profile.html, etc. It’s important to use the same name for all of the files in the component folder and to match the TypeScript class name.
- Class names:
With Angular 20, the files created with the CLI may omit the component or service extension in file and class name, but in order to identify them, you should add those extensions yourself, or change configuration. Namely,
- user-profile.ts —> user-profile.component.ts
- UserProfile —> UserProfileComponent
- user-management.ts —> user-management.service.ts
- UserManagement —> UserManagementService
Angular still supports them, so you can safely use them 🙂.
- Class selectors
For component selectors, you will usually see them named like the file names with an “app” prefix, namely “app-user-profile”. It’s the default selector style which can be edited, but it’s still best to keep it that way in order to comply with Angular style guide 🙂.
For pipes created with CLI you’ll see the name written with camelCase, e.g customPipe. This is also the default and camelCase is widely preferred and encouraged, therefore I encourage you to stick to this.
Directives have attribute selectors like appUserProfile written in camelCase.
When you write your program, camelCase is widely preferred for property and function names within classes. You shouldn’t use camelCase for class names but you should use it for everything inside the class. For example:
// user-profile.component.ts
import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { User } from './user.model';
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrl: './user-profile.component.css',
imports: [CommonModule],
})
export class UserProfileComponent {
readonly user = input.required<User>();
editRequest = output<number>();
private _lastEditRequestTime: Date | null = null;
onEditClick(): void {
this._logEditRequest();
this.editRequest.emit(this.user().id);
}
private _logEditRequest(): void {
this._lastEditRequestTime = new Date();
console.log(`Edit requested at: ${this._lastEditRequestTime.toISOString()}`);
}
}
For private properties and methods, it’s a good practice to add an underscore “_” or hash “#” before the name to differentiate them from the public properties. Also notice how properties and methods, public and private are grouped together.
Organization of project files
For large projects, keeping a messy folder tree will make it difficult to manage your folders. It’s always a good practice to structure them. This section will talk about some rules to follow for project folders. Firstly, all of the application’s code goes inside the src folder, including app.config.ts and bootstrap files. main.ts is the file for bootstrapping your application. When you create a project using the CLI, you will see an app folder inside src, which contains the component files bootstrapped in main.ts. While it’s app that is the root folder for your components, any other component you create should go inside a folder of its own, located inside app. Namely,
src/
|
├── app/
| |
| ├── core/
| | └── services/
| | └─ logger.service.ts
| |
| ├── features/
| | └── user-profile/
| | ├── components/
| | | └── user-profile.component.ts
| | ├── models/
| | | └─ user.model.ts
| | └── services/
| | └─ user.service.ts
| |
| ├── shared/
| | ├── components/
| | | └── header/
| | | ├── header.component.html
| | | └── header.component.ts
| | └── services/
| | └─ analytics.service.ts
| |
| ├── app.component.css
| ├── app.component.html
| ├── app.component.ts
| ├── app.config.ts
| └── app.routes.ts
|
├── environments/
| ├── environment.development.ts
| └── environment.ts
|
├── index.html
├── main.ts
└── styles.css
On a side note, if you will be using test files, you should keep them inside the relevant component folder. You might think this folder tree is so tall because there’s one separate file for everything. In this case, you shouldn’t worry about this because you should in fact limit one concept to one file (i.e, don’t keep models, components, services in the same file). The exception to this is that when some classes are very small, you can store them in one file as long as they’re relevant. This is a practice that’s okay to do if your project is very large and it’ll be efficient to reduce the amount of files. However, as a beginner, you should feel comfortable with the conventions before going into bigger projects.
Debugging
Developer tools (F12 or Ctrl+Shift+I for Windows/Linux; Fn+F12 or Cmd+Option+I for Mac) is a great set of tools for inspecting your application. Angular has a browser extension called Angular DevTools, that provides more advanced features that allow you to debug each component comfortably. See more here.
Summary
Components are the core building blocks of Angular projects. You might notice some tips for smaller applications that you have overlooked, but could help you while working on bigger projects. These practices include the single responsibility principle where you have to reduce a component’s job to only its own, registering the appropriate change detection strategy, enabling lazy loading, learning the naming conventions, tips for organizing your workspace, and more advanced debugging tools. Overall, you have learned about the best practices for building more readable, understandable, and robust Angular apps. Happy coding!