Angular
This section contains coding standards for Angular projects.
Please also follow other coding standards for consistent Angular code.
Project architecture
Use a monorepo
Use a monorepo to manage your Angular projects. This allows you to share code between projects, and makes it easier to manage dependencies.
Use abstraction layers
Use abstraction layers to separate concerns. This makes your code more maintainable and testable. It also makes it easier to replace parts of your codebase.
Minimize bundle size
For an introduction on how to minimize your Angular bundle size, check out this video.
Analyze your bundle
Periodically check the bundle size of your application. You can analyze your Angular bundle using 3rd party plugins. One of those is source-map-explorer.
Use tree-shakeable libraries
Tree-shaking removes unused code from your bundles. Not all libraries are tree-shakeable. Only use libraries that are capable of tree-shaking.
Lazy load images
Use the NgOptimizedImage directive to optimize using images in your application.
Routing
Lazy load modules
Lazy-load feature modules in your router (docs):
const routes: Routes = [
{
path: 'items',
loadChildren: () => import('./items/items.module').then((m) => m.ItemsModule),
},
];
Lazy load components
Lazy-load components in your routes:
const routes = [
{
path: 'search',
loadComponent: () => import('../search').then((m) => m.SearchComponent),
},
];
Don't use route resolvers
Resolvers are a way to fetch data before a route is activated. This blocks the route from activating until the data is fetched. This is an anti-pattern, especially in Angular and reactive programming.
You should always eagerly load components, and load your data in the correct lifecycle methods of your component.
This way, the user can navigate to the route immediately, and data will be fetched in the background, allowing you to easily show a loading indicator, skeleton loaders, or other UX patterns.
You should only use resolvers when you can't fetch data in the component itself. These use-cases are very rare.
Change detection
Angular has two modes for change detection: Default and OnPush. Components that implement the Default strategy re-render when a change detection (CD) cycle runs. In OnPush strategy, change detection only propagates up the view tree from the current view to the topmost. Check out this page for more information.
Use OnPush change detection strategy by default
To prevent unnecessary rendering of your components, all your components should use the OnPush strategy for ChangeDetectionStrategy by default. You do this by adding the property to the @Component decorator for your component:
@Component({
// ...other properties
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {}
Using this strategy, the component will trigger its change detection in the following cases:
- Whenever the value for an @Input changes
- When events that you subscribe to using () in your template, or @HostListener in your code happen
- When change detection is triggered manually, using the detectChanges() method of the ChangeDetectorRef
Schematics
You can setup your project to use the OnPush strategy as default when generating components through CLI commands. Add the following to your angular.json or project.json files:
{
"schematics": {
"@schematics/angular:component": {
"changeDetection": "OnPush"
}
}
}
Find more in depth information about this topic in this article.
Run code outside NgZone
For performance sensitive actions, like handling events that trigger rapidly (like mousemove and scroll), consider running those operations outside the NgZone. Changes outside the zone will not trigger ticks in the change detection mechanism.
Find more information in this article.
Reactivity
Prefer observables over promises
A Promise can not be cancelled, and you can not easily repeat or retry them. Using promises instead of observables should be considered an anti-pattern in an Angular context. RxJS is deeply integrated in Angular and you should be (or become) familiar with working with observable streams, anyway.
Use pure functions and pipes
Use pure pipes that can be memoized. They don’t re-render if the input params have not changed.
Styles
Use scoped styles
Use view encapsulation by default. This keeps your styling scoped to the specific component, prevents style leaking to other elements, and allows you to keep your selectors short.
Don't use deprecated selectors
Don't use ::ng-deep or /deep/ to target elements inside or outside of your component. These are deprecated and will be removed in the future.
Keep import paths short
To be able to easily use your shared SCSS resources, you can add include paths to your project.json file. In this example, we have a shared library that has some mixins we want to import in our project.
Normally, we would import styles using relative paths. Alternatively, add the following to your path-to-app/project.json file:
{
"build": {
"stylePreprocessorOptions": {
// can contain multiple import paths
"includePaths": ["libs/path-to-library/src/styles"]
}
}
}
Now when we want to use any file inside that folder, we can use this import statement:
/* 🚫 Ugly long relative import path */
@use '../../../libs/path-to-library/src/styles/reset.scss';
@use '../../../libs/path-to-library/src/styles/mixins.scss';
/* ✅ Awesome short import path! */
@use 'reset';
@use 'mixins';