Components serve as the core structural units in Angular applications. This article outlines a set of recommended practices that contribute to cleaner code and a more structured project layout. The discussion focuses on component responsibilities, naming conventions, and how to organize components effectively.
Single Responsibility Principle (SRP)
Implementing SRP within your codebase guarantees that every class (whether components, services, pipes, directives, or other entities) is dedicated to a single task—like displaying a list or collecting form inputs. To put SRP into action, your first step is to identify which pieces act as smart components versus dumb ones, and specify the role of each. Adopting this principle brings several advantages, such as:
- corresponding to a reduction in file size, since fewer features are handled,
- lowering the risk of unintended side-effects and defects,
- minimizing the edits required within classes because their scope is narrower,
- accelerating the overall development workflow,
- simplifying comprehension of the codebase
Take a scenario where we build a recipe storage application. Such an application includes a selection list and another area for viewing, updating, or creating recipes. Assume its components are NavbarComponent and ViewComponent. NavbarComponent handles the site branding and page navigation, while ViewComponent deals with both displaying the recipes and supporting add/edit/delete actions triggered by user interactions, which encompasses rendering the item list and capturing form data. As it stands, ViewComponent is overloaded with duties.
How to apply SRP?
The right approach is to break ViewComponent into three separate pieces:
- RecipesListComponent – positioned in the sidebar to showcase the recipes,
- RecipeCardComponent – responsible for presenting details on the right,
- RecipeFormComponent – dedicated to processing user input.
On top of that, introducing a RecipeManagementService allows the app to manage CRUD operations on recipes. This lets AppComponent handle interactions with those sub-components via signals, leaving the others as pure presentational elements.
Change Detection Strategy
Change detection strategies in Angular dictate whether a component needs to be examined for modifications. Two strategies exist: Default and OnPush. You can think of Default as a nurse who visits every patient on the ward, whereas OnPush resembles a nurse who only checks on those whose condition has changed otherwise, or when explicitly requested.
When Default is active, every child component gets evaluated as soon as its parent is evaluated. Consequently, modifying any section of the app results in Angular inspecting all components across the entire hierarchy. This setup is suboptimal since it triggers checks in components that have remained untouched. Typical instances that activate Default include HTTP calls, timers, and user interactions.
OnPush, in contrast, constrains what gets checked to reduce unnecessary evaluations. Part of OnPush is the CheckOnce strategy—a component receives a single check when its dirty flag is set, then is omitted for the duration of execution. For large-scale apps, OnPush delivers a noticeable boost in performance, particularly on mobile platforms. If a child triggers a change, each of its ancestors gets marked dirty sequentially and moves through change detection. Here’s an example of this behavior:
AppComponent
HeaderComponent
ContentComponent
TodoListComponent
TodoComponent
In the component tree above, every component is configured with OnPush. Now, consider that a change occurs within TodoComponent:
Root Component -> LViewFlags.Dirty
|
...
|
ContentComponent -> LViewFlags.Dirty
|
|
TodoListComponent -> LViewFlags.Dirty
|
|
TodoComponent (event triggered here) -> markViewDirty() -> LViewFlags.Dirty
Zone.js detects the changes made inside TodoComponent and notifies the root component accordingly. However, the mutations in TodoComponent mark it as dirty, and this dirty flag propagates upward through all its ancestors. This means the entire chain of components gets flagged, giving Zone.js a clear picture of which components require further examination. In this scenario, HeaderComponent stays out of the picture, since neither it nor any of its child components have registered any changes. Thanks to its OnPush strategy, it gets skipped entirely.
For larger applications, adopting the OnPush change detection strategy trims the number of components that need re-checking when an asynchronous event occurs, boosting overall efficiency. The common recommendation is to apply OnPush to every component in your project, since it carries virtually no overhead and can lead to noticeable time savings. 🙂
For a deeper dive and practical examples, take a look at this resource and this one.
Lazy loading
The lazy loading approach lets you defer the loading of certain elements until they are actually needed, rather than pulling everything in at startup. This strategy cuts down the initial bundle size, translating into a faster first render. Keep in mind that deferred elements might require a bit more time to appear. The core idea behind lazy loading is to communicate priorities to Angular—what must be present right away and what can wait. As your application expands, so does the volume of data it handles. Therefore, utilizing this technique is wise for keeping initial load times reasonable, instead of forcing everything to load simultaneously.
To start, ensure all your page-level components are lazy-loaded within the route configuration. This is achieved by using the lazy loading mechanisms in the router via loadChildren():
{
path: 'heavy',
loadChildren: () => import('./heavy/heavy.module').then(m => m.HeavyModule)
}
Angular also lets you defer loading inside components, which you can achieve with @defer blocks in the following way:
@defer {
<large-component />
}
When you place components, services, and directives within an @defer block, they get bundled into distinct JavaScript chunks that fetch only when needed, once the app has bootstrapped. Yet this behavior applies exclusively to standalone components; any component that isn't standalone loads eagerly despite the deferral. For lazy loading a module via @defer, you must construct a standalone wrapper component around 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);
}
}
Once that’s done, the component wrapper can be placed inside the @defer block.
Naming conventions
This part focuses on naming conventions that enhance code readability. Angular ships with its own style guide, which is automatically applied to project files generated via the Angular CLI. We’ll start with the Angular Style Guide, then move on to a few additional naming practices worth keeping in mind.
- File names:
When a component name spans multiple words, the file name should place a hyphen “-” between each word—for example, user-profile.ts, user-profile.html, and so on. Consistency is key: all files inside the component folder must share the same base name and align with the TypeScript class name.
- Class names:
Starting with Angular 20, CLI-generated files can drop the component or service suffix from both file and class names. To keep things identifiable, you should either append those extensions yourself or adjust the configuration. Concretely:
- user-profile.ts —> user-profile.component.ts
- UserProfile —> UserProfileComponent
- user-management.ts —> user-management.service.ts
- UserManagement —> UserManagementService
Angular continues to support the shorter versions, so you’re free to keep them if you prefer 🙂.
- Class selectors
Component selectors typically mirror the file name but carry an “app” prefix, such as “app-user-profile”. This is the default selector style and can be customized, yet staying with it aligns with the Angular style guide, which is advisable 🙂.
Pipes generated via the CLI adopt a camelCase convention, e.g customPipe. This default style is widely endorsed and recommended, so I suggest sticking with it.
Directives use attribute selectors in camelCase, like appUserProfile.
Within your code, camelCase is the preferred style for property and function names in classes. Avoid camelCase for class names themselves, but apply it to everything inside the class. 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()}`);
}
}
It is considered good practice to prefix private properties and methods with an underscore “_” or a hash “#” so they stand apart from their public counterparts. Moreover, take note of how the properties and methods, whether public or private, are organized into distinct groups.
Organization of project files
When dealing with large-scale projects, a cluttered directory structure can quickly become unmanageable. Structuring your folders is always a wise approach. In this section, we will outline a few guidelines for arranging project folders. To start, all application code—including app.config.ts and bootstrap files—resides within the src directory. The main.ts file is responsible for bootstrapping the application. After generating a project with the CLI, you will find an app folder inside src that holds the component files which are bootstrapped in main.ts. While app serves as the root folder for your components, every additional component you create should be placed in its own dedicated subfolder within app. Specifically,
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
As an aside, if you plan to write test files, place them in the same folder as the relevant component. You may worry that this creates a deep folder structure because each item has its own file. That’s not a concern, though, because it’s best to keep one concept per file (for instance, avoid mixing models, components, and services together). The only exception is when several small, related classes can live together in a single file. This approach is acceptable for large projects where reducing the file count improves efficiency. Still, as a newcomer, you should first get comfortable with these conventions before tackling bigger codebases.
Debugging
Browser developer tools (opened with F12 or Ctrl+Shift+I on Windows/Linux, or Fn+F12 or Cmd+Option+I on Mac) are excellent for inspecting your app. Additionally, Angular offers a browser extension named Angular DevTools, which adds advanced capabilities for debugging individual components with ease. For more information, check out this link.
Summary
Components form the foundation of any Angular project. Some tips may seem geared toward smaller apps and go unnoticed, yet they prove valuable as your projects scale up. These include adhering to the single responsibility principle to narrow a component’s duties to itself, choosing the right change detection strategy, enabling lazy loading, following naming conventions, structuring your workspace thoughtfully, and utilizing enhanced debugging tools. In the end, you now understand the best practices for creating Angular applications that are more readable, maintainable, and reliable. Happy coding!
