Angular is a comprehensive JavaScript framework that is frequently used by developers for building cross-platform applications. Angular applications are quick, light, and simple-to-use. Therefore Angular is one of our favourite frameworks for web applications development.
But we should take into account the many subtleties due to which the application can turn into a continuous mess.
So let's begin!
app/
├── animations/
├── components/
| ├── main/
| ├── profile/
| ├── shop/
| ├── ...
├── modules/
| ├── admin/
| ├── sections/
| | ├── section1/
| | ├── section2/
| | ├── section3/
| | ├── shared/ => shared only between these sections
| | ├── section.module.ts
├── directives/
├── pipes/
├── models/
├── resolvers/
├── services/
├── guards/
├── store/
| ├── actions/
| ├── reducers/
├── assets/
| ├── images/
| ├── svg/
| ├── sprites/
| ├── SCSS/
| | ├── media_queries/
| | ├── _components.scss
| | ├── _base.scss
| | ├── _buttons.scss
| | ├── _cards.scss
| | ├── ...
| | └── main.scss
├── shared/ => shared through all app
| ├── components/
| ├── pipes/
| ├── directives/
| ├── shared.module.ts
app.component.html
app.component.ts
app.module.ts
NOTE: such structure may not be suitable for all projects.
Angular will re-render only those elements that have changed rather than whole DOM.
// Template
<li *ngFor="let product of products; trackBy: trackByFn">{{ product.title }}</li>
// Component
trackByFn(index, product) {
return product.id; // must be unique
}
async
pipes unsubscribe themselves automatically. It makes component more clean and prevents memory leaks if you forget to unsubscribe manually.
// Template
<p>{{ iAmObservable$ | async }}</p>
// Component
this.iAmObservable$ = this.yourService.someObservable();
Forgetting to unsubscribe from observables will lead to memory leaks even after a component has been destroyed.
ngOnInit() {
this.subscription = this.yourService.someObservable().subscribe(...);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
- or you can consider the following approach
ngUnsubscribe: Subject<void> = new Subject();
ngOnInit() {
yourObservable.pipe(takeUntil(this.ngUnsubscribe)).subscribe();
}
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
Operators like { first, take, takeWhile }
might help you in some circumstances.
Let's consider the most common operators (functions) and the differences between them.
throttleTime
vsdebounceTime
// wait 2 seconds before emit a new value
this.someObservable.pipe(throttleTime(2000))
// new value will be emitted only if nothing happens for 2 seconds
this.someObservable.pipe(debounceTime(2000))
distinctUntilChanged
:
searchField: FormControl = new FormControl();
// new value will be emitted only if it isn't the same as previous with 2 seconds delay
ngOnInit() {
this.searchField.valueChanges.pipe(debounceTime(2000), distinctUntilChanged()).subscribe(...);
}
mergeMap
:
if you want to concurrently handle all the emissions
field1: FormControl = new FormControl();
field2: FormControl = new FormControl();
// let's say "field1" you fieled with "hello" and "field2" with "world"
ngOnInit() {
this.field1.valueChanges.pipe(
mergeMap(value1 => this.field2.valueChanges.pipe(
map(value2 => `${value1}--${value2}`)
))
).subscribe(...); => final value is "hello--world"
}
NOTE: A new value will be emitted only when all observables have emitted new value
switchMap
:
if you want to ignore the previous emissions when there is a new emission. It means when new value emits the previous subscription will be cancelled.
someObservable1.pipe(
switchMap(event => someObservable2)
).subscribe(...);
concatMap
:
if you want to handle the emissions one after another as they are emitted. It means someObservable2
will emit new value in strict sequence one by one.
someObservable1.pipe(
concatMap(event => someObservable2)
).subscribe(...);
exhaustMap
:
if you want to cancel all the new emissions while processing a previous emission. It means someObservable2
will listen to new emission only when previous is finished.
someObservable1.pipe(
exhaustMap(event => someObservable2)
).subscribe(...);
I strongly advise to go through this documentation.
NOTE: Avoid having subscriptions inside subscriptions.
Perhaps one of the most useful features. Most likely that users will not visit all pages of the site, therefore consider splitting the app into small modules, decreasing the main bundle size. It can also help users with a weak internet connection to load the site faster.
# app.routing-module.ts
const routes: Routes = [
{
path: 'admin',
loadChildren: '/path/to/admin/module#AdminModule'
}
];
By splitting our application into stand-alone modules we can lazy load the module when the user clicks the link leading to this module. But we can go further using build into Angular preload strategy or defining our own. Preload strategy gives us an ability to load all or some of modules in the background. It means Angular immediately starts rendering modules instead of waiting for the module to download over the network.
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
exports: [RouterModule],
providers: [AppCustomPreloader]
})
export class AppRoutingModule { }
But this strategy PreloadAllModules
may not be the best solution since Angular will load all modules, even those that the user visits very rarely. What we can do?
Lets define our own Preload Strategy.
# app.routing-module.ts
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{
path: 'profile',
loadChildren: '/path/to/profile/module#ProfileModule',
data: { preload: true } // => will be preloaded in the backround
},
{
path: 'admin',
loadChildren: '/path/to/admin/module#AdminModule' // => won't be preloaded in the backround
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: CustomPreloader })],
exports: [RouterModule],
providers: [CustomPreloader]
})
export class AppRoutingModule { }
# ./custom-preload-strategy.ts
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
export class CustomPreloader implements PreloadingStrategy {
preload(route: Route, load: Function): Observable<any> {
return route.data && route.data.preload ? load() : of(null);
}
}
Quite often API calls might fail. Consider building some logic to handle them.
ngOnInit() {
this.productService.allProducts.retryWhen(this.handleRetry()).subscribe(...);
}
handleRetry() {
return err => {
return err.scan(count => {
count++;
if (count < 6) {
return count;
} else {
throw(err);
}
}, 0).delay(5000);
};
}
NOTE: This is just a basic example. A real project could have more complex logic.
myVar: string = 'Ruki bazuki';
someArray: Array<number> = [1, 2, 3];
anotherVar: 'a'|'b' = 'a';
Always use lint rules. Find more here.
Components should only deal with the display logic. Try to make them reusable if possible.
Do not make multiple unnecessary API calls to the same endpoint if data doesn't change often. Check if requested data is already present and update it only when necessary.
// Template
<p *ngIf="showMe"> Hello world </p>
// Component
showMe (): boolean {
return couldShow ? true : false;
}
@ngrx/store can help maintain the state of your application more easily, keeping state related logic separately, reducing complexity. It also has a nice memoization mechanism which might save your app performance.
-
Webpack Bundle Analyzer. Visualize size of Webpack output files with an interactive zoomable treemap. Docs.
-
Lazy loading for images. Well explained here.
-
Virtual Scrolling. Find more info here.
-
Font loading strategy. Nice article about the topic here.
In case users are using not only Google and performance on mobile and low-powered devices is a must have, you might consider using Server-Side Rendering (SSR). Read more about Angular Universal here.
The names and logos for Codica are trademarks of Codica.
We love open source software! See our other projects or hire us to design, develop, and grow your product.