Ренесанс у світі Angular: зміни, які міняють правила гри
Привіт! Мене звуть Олексій Горбунов, я займаюся фронтенд-розробкою в Levi9. На нещодавньому Frontend Meetup ми з колегами говорили про нові можливості розробки на Angular, і це доволі гаряча тема зараз. Angular продовжує швидко розвиватися та зростати. Програмісти Google вводять все більше фішок як для досвічених розробників, так і для початківців.
У цій статті я хочу виділити дві найважливіші фічі, які мене зацікавили в 15 версії цього фреймворку. Вважаю, що вони можуть змінити багато проєктів і вивести перевикористання коду на новий рівень. Разом з цим я хочу розглянути, що нам приготував Angular 16, який вийшов нещодавно, та як це змінить розробку, взаємодію з даними й компонентами в майбутньому.
Angular Standalone Components API
Для початку хочеться сказати, що і до developer preview ми могли будувати майже standalone components за допомогою SCAM-паттерну — Single Component Angular Modules.
import { Component, Input } from '@angular/core';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'hello',
template: `Hello {{name}}!`,
styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent {
@Input() name: string;
}
@NgModule({
imports: [CommonModule],
declarations: [HelloComponent],
exports: [HelloComponent],
})
export class HelloModule {}
Такий підхід мав свої мінуси:
- Ми все ще використовували
@NgModule; - Синтаксис трохи вводив в оману та був незручний.
У
Отож, що нам принесуть Standalone Components
1. Легший поріг входження
Саме так: спеціалісти, що пишуть на React і Vue, часто не хочуть зв’язуватися з Angular через відмінну від них структуру та інший підхід до побудови інтерфейсів. Може відлякувати це й початківців, зокрема своєю навантаженістю та складністю. Зі Standalone Components розробники більш гнучкі та вільні щодо ознайомлення з фреймворком, побудовою власної архітектури тощо — хоча я вважаю, що це часто не є найкращий варіант. Водночас однією концепцією для розуміння та розбору менше. Модулі — це доволі велика тема та додаткове навантаження для новачка, що може відштовхнути його від фреймворку. Команда Angular виправляє це й робить життя початківців в ІТ простішим.
2. Менше Boilerplate коду
Ми не можемо визначити один і той самий компонент у двох різних модулях. Приклад:
@NgModule({
declarations: [ComponentWeNeedToDeclare]
// ...
})
export class FirstModule {}
@NgModule({
// This causes an error because ComponentWeNeedToDeclare already
// belongs to FirstModule.
declarations: [ComponentWeNeedToDeclare]
// ...
})
export class SecondModule {} Для використання компонента в багатьох місцях ми маємо зробити так званий Feature Module над ним і тільки тоді зможемо використовувати компонент в інших незалежних модулях:
// Since ComponentWeNeed component's functionality doesn't
// match existing Feature modules, we must create a
// new one
@NgModule({
imports: [ComponentWeNeed],
// ...
})
export class ComponentWeNeedModule {}
Зі Standalone API все простіше: якщо визначити компонент як Standalone, то ми зможемо просто додати його в різні модулі через проперті import.
@NgModule({
import: [ComponentWeNeed],
declarations: [
SomeComponentOne,
SomeComponentTwo,
// ...
]
// ...
})
export class MenuModule {}
@NgModule({
import: [ComponentWeNeed],
declarations: [
SomeUserComponentOne,
SomeUserComponentTwo,
// ...
]
})
export class UserModule {}
3. Легше створення самостійних компонентів
Припустимо, ми хочемо створити компонент, який використовуватиме Matlist із Material UI. Тоді нам не обійтися без створення модуля для цього компонента та імпорту MatListModule.
// Module for import all needed dependencies
@NgModule({
imports: [
MatListModule,
CommonModule,
],
// ...
})
export class DesiredNgModule {} // Component for solving my problem
@Component({
selector: 'app-not-standalone-component',
template: `
<mat-list>
<mat-list-item *ngFor="let item of toDoItems$ | async">
{{item.task}}
</mat-list-item>
</mat-list>
`
})
export class NotStandaloneComponent {
toDoItems$ = of([
{task: 'Make article'},
{task: 'Eat pizza'},
{task: 'Buy a Rolls-Royce'},
]);
} Тобто в нас вже буде мінімум два файли, що залежать один від одного. Зі Standalone Components все набагато простіше: ми вказуємо залежності безпосередньо в компоненті.
@Component({
selector: 'app-standalone-component',
standalone: true,
imports: [
MatListModule,
CommonModule,
],
template: `
<mat-list>
<mat-list-item *ngFor="let item of toDoItems$ | async">
{{item.task}}
</mat-list-item>
</mat-list>
`
})
export class StandaloneComponent {
toDoItems$ = of([
{task: 'Make article'},
{task: 'Eat pizza'},
{task: 'Buy a Rolls-Royce'},
]);
}4. Простіший Lazy-Loading та роутинг
Припустимо, в нас є сайт і компонент профілю користувача, який ми хочемо під’єднати асинхронно. В такому разі для підключення нам потрібно створити додатковий файл:
// src/app/app-routing.module.ts
const routes: Routes = [
{
path: 'user',
loadChildren: () => import('./user/user.module')
.then(m => m.UserModule)
}
];
// src/user/user-routing.module.ts
const routes: Routes = [
{
path: '',
component: UserComponent
}
]
@NgModule({
imports: [UserModule.forChild(routes)],
exports: [UserModule]
})
export class UserRoutingModule { }
Зі Standalone Components це буде лише рядок у головному роутингу:
const routes: Routes = [
{
path: 'user',
loadComponent: () => import('./user/user.component')
.then(m => m.UserComponent)
}
]Міграція на Standalone Components API
Насправді міграція на Standalone Components не така складна. Проте, важливо заздалегідь визначитися, для яких місць застосунку вам це може знадобитися та чи потрібно в цілому. Як мінімум для невеликих проєктів це рішення гарне, тож розберімось, що потрібно буде поміняти.
Разом з цим хотілось би сказати, що у своєму застосунку ви можете мати як компоненти з модулями, так і Standalone Components. Вам не обов’язково мігрувати все і одночасно, робіть це там, де потрібно. Команда Angular наголошувала на тому, що NgModule залишаються й будуть з Angular ще довго.
Ділюся також гарним дослідженням, яке показує, що бандл застосунку на Standalone Components важить менше, ніж застосунок на звичних нам модулях — почитати можна тут.
Що ж, перейдімо до міграції.
Main.ts
Якщо ми будуємо застосунок на Standalone Components і в нас перший вхідний компонент також Standalone, то потрібно по-іншому запускати застосунок у файлі main.ts. Потрібно викликати метод bootstrapApplication замість bootstrapModule, передати в нього головний компонент, як перший параметр, а як другий — об’єкт, в якому в масиві providers вказати всі головні інтерсептори, сервіси та масив роутів — про це далі.
// without standalone
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
// with standalone
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom([BrowserAnimationsModule]),
{ provide: BACKEND_API, useValue: "https://dummyjson.com" },
{ provide: ToastrService, useClass: ToastrService },
{ provide: AuthService, useClass: AuthService },
provideHttpClient(
withInterceptors([errorInterceptor, authInterceptor, cacheInterceptor])
),
{ provide: ErrorHandler, useClass: GlobalErrorHandlerService },
provideRouter([...APP_ROUTES]),
],
});Роутинг
Ми відразу хочемо також використовувати в роутах Lazy Loading для зменшення бандла та поділу програми на чанки. Розглянемо якраз на прикладі з nested-роутами
В src/app створюємо файл app-routes.ts
// src/app/app-routes
export const APP_ROUTES: Route[] = [
{
path: "",
pathMatch: "full",
component: HomeComponent,
},
{
path: "posts",
canActivate: [AuthGuard],
loadChildren: () =>
import("@app/post-feature/post-routes")
.then((mod) => mod.POST_ROUTES),
},
// .... ,
{
path: "**",
loadComponent: () =>
import("@app/_pages")
.then((mod) => mod.PageNotFoundComponent),
},
];
Зауважу, що якщо ми хочемо завантажувати компонент ліниво, то пишемо loadComponent. Далі ви можете помітити, що у нас є також loadChildren, і там ми завантажуємо масив роутів для посту.
Щоб це зробити, треба створити файл src/post-feature/post-routes.ts. Далі створюємо роути для постів і експортуємо цей масив:
// src/post-feature/post-routes.ts
export const POST_ROUTES: Route[] = [
{
path: "",
pathMatch: "prefix",
providers: [{ provide: PostService, useClass: PostService }],
children: [
{ path: "", component: PostListComponent },
{ path: ":id", component: PostDetailComponent },
],
},
];
Загалом... Все. Це основні поінти перенесення програми на Standalone Components API. Як на мене, це додає певної свіжості в Angular та урізноманітнює розробку на цьому фреймворку. Правда, місць, де новачки можуть накосячити, стає ще більше)))) Тепер перейдімо до наступного топіку.
Directive composition API
У 15 версії Angular було представлено нове API, яке допоможе бібліотекам на Angular набути нових, функціональніших директив і конфігурабельних компонентів, що більше враховують користувацький досвід із застосунком. Як композиція директив зможе допомогти нам у повсякденних задачах — розберемося на практиці.
Розбираємося на практиці
Припустимо, маємо дві директиви. Перша називатиметься MakeBoldDirective — при наведенні вона робитиме текст в елементі жирним. Додатково при наведенні ми викликатимемо функцію, яку передаватимемо через @Output.
@Directive({
selector: '[makeBold]',
standalone: true,
})
export class MakeBoldDirective {
@Output() hover = new EventEmitter();
constructor(private hostElement: ElementRef) {}
@HostListener('mouseenter')
onMouseEnter() {
this.hostElement.nativeElement.style.fontWeight = 'bold';
this.hover.emit();
}
@HostListener('mouseleave')
onMouseLeave() {
this.hostElement.nativeElement.style.fontWeight = 'normal';
}
}
Припустимо також, що маємо директиву UnderlineDirective, яка при наведенні підкреслює текст певним кольором.
@Directive({
selector: '[appUnderline]',
standalone: true,
})
export class UnderlineDirective {
@Input() color = 'black';
constructor(private hostElement: ElementRef) {}
@HostListener('mouseenter')
onMouseEnter() {
this.hostElement.nativeElement.style.textDecoration = 'underline dotted';
this.hostElement.nativeElement.style.textDecorationColor = this.color;
}
@HostListener('mouseleave')
onMouseLeave() {
this.hostElement.nativeElement.style.textDecoration = 'none';
this.hostElement.nativeElement.style.textDecorationColor = 'none';
}
}
За логікою, якщо ми тепер хочемо зробити, щоб елемент був і підкреслений, і текст всередині був жирний, треба застосовувати дві директиви. При цьому ще може бути умовна tooltip directive — це якщо ми хочемо, щоб компонент ставав жирним, підкресленим і щоб у нього випливала підказка. І так далі.
Застосування величезної кількості директив — це марудна й незручна справа. Щобільше, назви інпутів можуть дублюватися і для різних директив ми захочемо передати різні значення. Ось тут нам і допоможе вся сила Directive Composition API
Пропоную об’єднати BoldDirective з UnderlineDirective в одну директиву MouseenterDirective і подивитися, як це виглядає.
@Directive({
selector: '[makeBoldAndUnderline]',
standalone: true,
hostDirectives: [
{
directive: BoldDirective,
outputs: ['hover: boldHoverEvent'],
},
{
directive: UnderlineDirective,
inputs: ['color: underlineColor'],
},
],
})
export class MouseenterDirective {
constructor() {}
}
Ось так ми можемо застосувати директиву до компонента в темплейті:
<app-my-component makeBoldAndUnderline underlineColor="red" (boldHoverEvent)="hoverEventHandler()"> </app-my-component>
Як бачимо, ми передали наші директиви у масив hostDirectives і це проперті тепер також є в компонентах. Детальніше про hostDirectives трохи згодом.
До речі, у цьому випадку порядок виконання логіки директив буде таким:
- BoldDirective.
- UnderlineDirective.
- MouseenterDirective.
Такий порядок зберігається для всіх фаз: instantiation ⇒ receiving inputs ⇒ host binding.
Але цей запис теж неідеальний, якщо ми хочемо застосувати нашу композицію директив до конкретного компонента у багатьох місцях застосунку. Виходить, що в шаблоні треба прописувати назву директиви кожен раз. Це також можна покращити, використавши проперті hostDirectives вже в компоненті.
Далі розберемося, що це за проперті та, на додачу, покращимо наш код.
Проперті hostDirectives та як ним користуватись
Це проперті у нас з’явилось для директив та компонентів у 15 версії. Подивімось на її сигнатуру з репозиторію Angular.
hostDirectives?: (Type<unknown> | {
directive: Type<unknown>;
inputs?: string[];
outputs?: string[];
})[]Видно, що це масив об’єктів. В полі directive ми повинні передати директиву, а з полями inputs i outputs все цікавіше. Тут ми можемо задати аліас для інпутів і аутпутів, якщо дві директиви мають проперті, що називаються однаково, чи якщо просто хочемо, щоб якісь input чи output був очевиднішим та з більш зрозумілим неймінгом.
@Directive({
selector: '[makeBoldAndUnderline]',
standalone: true,
hostDirectives: [
{
directive: BoldDirective,
// color - input in directive, boldColor - alias for this input
inputs: ['color: boldColor']
// hover - output in directive, boldHoverEvent - alias for this output
outputs: ['hover: boldHoverEvent'],
},
],
})
export class MouseenterDirective {
constructor() {}
}
Тепер ми знаємо більше про hostDirectives. Далі покращимо наш код і застосуємо директиву MouseenterDirective до нашого компонента. Він очікуватиме передачі параметрів для цієї директиви відразу та, відповідно виглядатиме так:
@Component({
selector: 'app-my-component',
standalone: true,
imports: [CommonModule],
template: `<p>My component is working!</p>`,
styles: [],
hostDirectives: [MouseenterDirective],
})
export class TestComponent {
}
Там, де ми хочемо використовувати компонент (у шаблоні), ми можемо просто передати необхідні Input та Output — директива застосовуватиметься автоматично:
<app-my-component underlineColor="red" (boldHoverEvent)="hoverEventHandler()"> </app-my-component>
Такий підхід також має свої обмеження, про які варто пам’ятати:
- Директиви через
hostDirectivesзастосовуються до компонента під час компіляції, в runtime це конфігурувати чи варіативно передавати не можна. - Директиви, які ми передаємо в
hostDirectives,обов’язково повинні бутиstandalone: true. - Ми не можемо застосувати однакову директиву до компонента двічі. Якщо компонент у
hostDirectivesвже має директиву, а ми в темплейті її також застосовуємо, то фреймворк її проігнорує.
Тестування композиції директив
Я вже продемонстрував повну функціональність і сили нового API, але не можу оминути тестування композиції директив. Тут все максимально просто і робиться по аналогії з тестуванням звичайних директив через хост-компонент. Погляньмо на файл mouseenter.directive.spec.ts.
Тут нам знадобиться зробити два кроки:
1. Зробити хост-компонент.
@Component({
standalone: true,
imports: [
MouseenterDirective
],
template: `<span>Some text</span>`,
})
class TestUnderlineHoverComponent {
color = 'blue';
onHover() {
console.log('hover')
}
}
2. Написати тести до нього та перевірити все, що має виконувати директива (у коді прокоментований кожен рядок).
describe('MouseenterDirective', () => {
let component: TestUnderlineHoverComponent;
let fixture: ComponentFixture;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestUnderlineHoverComponent]
});
fixture = TestBed.createComponent(TestUnderlineHoverComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('text should be bold, underline blue, hover event triggered', () => {
// Getting out element by data attribute
const getElem = () => fixture.debugElement.query(By.css('[data-test]')) // Set spying on hover, state when our component should be changed
spyOn(component, 'onHover');
// Dispatching event of hover in test environment
getElem().nativeElement.dispatchEvent(new MouseEvent('mouseenter', {
view: window,
bubbles: true,
cancelable: true
}));
fixture.detectChanges();
// Checking if font is bold
expect(getElem().nativeElement.style.fontWeight).toBe('bold');// Checking if callback have been called expect(component.onHover).toHaveBeenCalledTimes(1);
// Checking if text is decorated (underlined by passed color)
expect(getElem().nativeElement.style.textDecorationColor).toBe('blue')
});
});
... І після цього ми отримуємо повністю протестовану директиву, що об’єднує декілька інших директив.
Сигнали в Angular і чому вони змінять все
15 лютого команда Angular відкрила свій перший PR, представила сигнали для фреймворку. Ця заява викликала активні обговорення в ком’юніті, зокрема дискусію в репозиторії Angular про сигнали та їхній вплив на взаємодію з фреймворком, що буде з RxJS, як тепер працювати з даними в компонентах тощо.
Крім того, сигнали вже з’явились у developer preview з 16 версією Angular.
Пропоную розглянути нові характеристики та як оновлення змінять наш підхід до розробки.
Навіщо все це
1. Поступово відмовитись від Zone.js. Колись це було проривом, але зараз Zone.js має недоліки, зокрема:
- Zone.js важить близько 100 кб. Для великих програм це небагато, але є суттєвим для невеликих, що вимагають швидкого завантаження.
- Коли відбуваються зміни в логіці, всі компоненти в дереві перевіряються. Відповідно неможливо просто визначити, який компонент був змінений без перевірки всіх складових.
- Zone.js все ще конвертує async-await у проміси навіть у браузерах, де вони підтримуються з коробки.
- Zone.js monkey-патчить браузерні об’єкти.
2. Іноді краще замінити RxJS і спростити життя розробників. RxJS дуже потужна бібліотека для реактивного програмування, але іноді вона ускладнює життя, адже треба звертати увагу на підписки та відписки від стрімів і особливості роботи з ними.
То що ж таке сигнали
Signals — це новий реактивний механізм Angular, який можна використовувати для створення реактивних значень для ще ефективнішої роботи з даними та стейтом. Сигнал негайно сповіщає всіх споживачів, коли його значення змінюється.
Angular покладається на сигнали, щоб зробити виявлення змін легшим і надійнішим.
Подивимося на практиці
Функція signal() створює сигнал типу WritableSignal. Додатково до функції отримання WritableSignal має додатковий API для зміни значення сигналу (разом із повідомленням будь-яких залежних компонентів про зміну). До них відносяться метод set для заміни значення сигналу, update для оновлення значення та mutate для виконання внутрішньої мутації поточного значення.
Розглянемо на прикладі:
const name = signal('Some value');
// in template we need to call name() to get value of signal
name.set('Some value 1');
name.update(str => `Updated value: ${str}`);Ми також можемо оновити значення сигналу через mutate
const carsToSell = signal<Car[]>([]);
carsToSell.mutate(list => {
list.push({
make: 'Rolls-Royce',
model: 'Wraith',
price: 350_000
});
});
Варто зазначити, що нам не потрібна імутабельність, щоб правильно повідомляти залежні від сигналу компоненти. Сигнали роблять це за нас.
Рівність значень і відстеження змін
У функцію створення сигналу можна передати об’єкт опцій, де вказуєте свою логіку порівняння двох значень сигналу. Функція-comparator використовуватиметься для трекінгу того, чи була зміна, і повідомлятиме інші компоненти відповідно до результату.
Якщо ж два значення (нове та старе) будуть однакові, відбуватимуться такі дії:
- Оновлення сигналу блокуватиметься.
- Компоненти не будуть повідомлені про зміни.
Приклад:
const animal = signal(
{type: 'cat', name: 'Murzich'},
{
equal: (prevValue, newValue) => {
return JSON.stringify(prevValue)
=== JSON.stringify(newValue)
}
}
);
Computed сигнали
Завдяки функції computed ми можемо приймати значення інших сигналів і створювати з них нові. Щобільше, створені з допомогою computed сигнали мемоїзовані, тобто не потрібно запускати перерахунок щоразу при отриманні значення. Він відбудеться лише при зміні залежних сигналів.
Приклад:
@Component({
selector: 'app-example',
standalone: true,
template: `
<div>Car: {{ car().make }}</div>
<div>Price in UAH: {{ priceUAH() }}</div>
`
})
export class App {
car = signal({make: 'Rolls-Royce', priceUSD: 350_000 });
priceUAH = computed(() => this.car().priceUSD * 36.5);
}
Оскільки priceUAH використовує car, який є сигналом, priceUAH оновлюється щоразу, коли змінюється car.
Іншими словами, priceUAH залежить від car або обчислюється з нього, і це працює щоразу, коли значення car змінюється.
Ефекти
Розглянемо одразу на практиці.
count = signal(0);
tripple = computed(() => this.count() * 3);
countType = computed(() => (this.count() % 2 === 0
? 'This is even'
: 'This is odd')
);
const effect = effect(() => {
console.log('Count changed', this.count());
console.log(this.count(), 'is', this.countType());
}); Функція всередині effect буде викликатися при будь-якій зміні, що відбувається в сигналах. У ефект можна буде додати кілька сигналів. Трохи схоже на роботу з useEffect у Reaсt, але має менше можливостей. Наприклад, ефекти можна використовувати для логування, синхронізації даних у localStorage, тощо. Але ефекти краще не використовувати для зміну стейту. Більше про це ви можете дізнатись з документації.
Функція effect повертає об’єкт з таким методом як destroy() для мануального знищення ефекту.
Функція untracked
Запобігає обчисленню будь-яких значень невідстежуваного сигналу. Це означає, що навіть якщо сигнал змінюється, споживачів не повідомляють про його зміну. Це може бути корисно, коли ми, наприклад, маємо знати про оновлення ціни акцій кожну секунду та оновлення ордерів кожну хвилину, і тоді на основі цього робити будь-який ефект.
Логічно обгорнути оновлення ціни в untracked і робити ефект раз на хвилину.
Приклад:
const trackedCounter = signal(0); const untrackedCounter = signal(0); // Executes when `trackedCounter` changes, // not when `untrackedCounter` changes: effect(() => console.log( trackedCounter(), untracked(untrackedCounter) ); trackedCounter.set(1); // logs 1 0 untrackedCounter.set(1); // does not log untrackedCounter.set(2); // does not log untrackedCounter.set(3); // does not log trackedCounter.set(2); // logs 2 3
RxJS + Signals = ❤️
Сигнали можна легко поєднувати з RxJS, адже команда Angular випустила з 16 версією пакет @angular/core/rxjs-interop з такими функціями як toObservable і toSignal. Очікується, що функція toObservable конвертуватиме сигнал в Observable, а toSignal робитиме зворотну дію.
Більше про цей пакет можна прочитати тут.
Функції toSignal і toObservable знадобляться нам у просунутіших сценаріях, як, наприклад, в коді нижче. В ньому маємо компонент, який показує зустрічі з календаря.
@Component([
// ...
])
export class AppointmentsSearchComponent {
private calendarService = inject(CalendarService);
// Signals
from = signal('01-03-2023');
to = signal('04-05-2023');
loading = signal(false);
// Observables
from$ = toObservable(this.from);
to$ = toObservable(this.to);
appointments$ = combineLatest({ from: this.from$, to: this.to$ }).pipe(
debounceTime(500),
tap(() => this.loading.set(true)),
switchMap(({from, to}) => this.calendarService.find(from, to)),
tap(() => this.loading.set(false))
);
// second parameter - initial value
appointments = toSignal(this.appointments$, []);
}
- Маємо сигнали
fromitoдля вибору діапазону дат, таloadingдля демонстрації завантаження. - Маємо також observable
from$ito$, в які попадають усі значення сигналів і тепер ми можемо водночас використовувати усю потужність RxJS. - Об’єднуємо
from$ito$і з дебаунсом завантажуємо нові зустрічі (appointments$) з календаря при зміні параметрівfromito, паралельно показуючи лоадер. - Далі конвертуємо наші завантажені зустрічі у сигнал
appointmentsдля подальшого використання. На основі нього ми також можемо, наприклад, відфільтрувати зустрічі по тегу за допомогою функціїcomputedтощо.
Quick recap по сигналах в Angular
Пропоную підсумувати, що ж принесуть нам сигнали.
- Більш гранулярні зміни. Згодом у нас з’явиться новий механізм для відстеження змін і фреймворк ставатиме продуктивніше. При цьому з’явиться багато ситуацій, де можна буде зробити наш наявний проєкт швидше.
- Новий спосіб роботи зі стейтом у компонентах. У нас з’являється новий зручніший механізм для роботи з даними всередині компонента. В ньому з коробки підтримуються computed значення, є ефекти, де можна використовувати async-await, а також не потрібно підписуватися та відписуватися від сигналів, адже за нас це робитиме Angular.
- Більш зручне створення складної логіки. Нам не потрібно буде використовувати стрім для простої логіки в застосунках, що суттєво прискорить розробку. Однак якщо в нас складно логічний сценарій, то ми легко зможемо трансформувати сигнал у стрім RxJS, виконати необхідні операції й одразу трансформувати його в сигнал.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

9 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів