Оновлений Angular Code Style: оглядаємо сигнали, standalone-компоненти та нові можливості
В Angular останнім часом з’явилося багато нового: від сигналів до оновленого стилю для шаблонів. Тож я подумав: можливо, настав час поділитися оновленим підходом до code style?
⚙️ Standalone
З випуском
Якщо у вас версія нижче 19, я рекомендую створювати виключно standalone-компоненти, уникаючи використання модулів.
// Standalone-компонент для версій Angular < 19 @Component({ standalone: true, selector: 'my-component', template: `Hello World!` }) // Standalone-компонент у Angular 19 @Component({ // прапор standalone більше не потрібен selector: 'my-component', template: `Hello World!` })
Порада! У Angular 19 з’явився прапор компілятора, який видасть помилку, якщо виявить компонент, директиву або пайп, що не є standalone.
{ "angularCompilerOptions": { "strictStandalone": true } }
⚙️ Signals
Ну тут все зрозуміло: стараємось більше використовувати сигнали, computed, а також input() та output().
@Component({ selector: 'my-example', template: ` <h1>{{ counter() }}</h1> <h2>{{ isEven() ? 'Even' : 'Odd' }}</h2> <span>{{ name }}</span> <button (click)="increment()">Increment</button> `, }) export class ExampleComponent { // Для отримання значень зовні readonly name = input<string>(); // Для відправки на зовні readonly clickBtn = output(); // Використовуємо signal для збереження значень readonly counter = signal(0); // Використовуємо computed для обчислення парності числа readonly isEven = computed(() => this.counter() % 2 === 0); // Збільшення значення сигналу increment(): void { this.counter.update(value => value + 1); this.clickBtn.emit() } }
Ви ж розумієте, що команда фреймворку рухається в бік відмови від Zone.js, тому що більше будете використовувати сигнали, то менше потрібно буде рефакторити код у майбутньому.
⚙️ Нова ера Lifecycle
Попрощайтеся з ngOnInit та ngAfterViewInit і оберіть спрощений підхід!
Ось базовий приклад, як було раніше з хуком ngOnChanges:
@Component({ standalone: true, selector: 'my-is-even', template: `<h1>Is Even: {{ isEven }}</h1>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class IsEvenComponent implements OnChanges { isEven: boolean | undefined; @Input({ required: true }) counter!: number; ngOnChanges(changes: SimpleChanges): void { if (changes['counter']) { this.isEven = changes['counter'].currentValue % 2 === 0; } } }
Тепер — як це написати по-новому:
@Component({ selector: 'my-is-even', template: `<h1>Is Even: {{ isEven() }}</h1>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class IsEvenComponent { counter = input.required<number>(); isEven = computed(() => this.counter() % 2 === 0); }
⚙️ Inject
Раніше для того, щоб заюзати наш DI, потрібно було в конструкторі прописувати залежності. Тепер у нас є новий метод inject!
// Раніше constructor(private userService: UserService) {} // Зараз private readonly userService = inject(UserService) // Дістати тільки одне проперті private readonly user = inject(UserService).user //У методі readonly user = this.getUser(); getUser(): User { return inject(UserService).user; }
Що він дає:
- inject зберігає правильний тип при використанні токенів;
- inject можна використовувати в інших функціях, що дає змогу створювати композиційні функції;
- при спадкуванні немає потреби передавати впроваджені сервіси до батьківського класу через виклик super в конструкторі.
Ін’єкція через конструктор залежить від прапора useDefineForClassFields, який потрібно явно встановити в значення false.
⚙️ Constructor
Окей, ми зрозуміли, що конструктор більше не є необхідним для inject. Але що ж робити з хуками afterRender та afterNextRender? А як щодо ефектів? Адже в документації зазначено, що їх потрібно інітити саме в конструкторі!?
@Component({ /* ... */ }) export class ExampleComponent { private readonly logProductChangesEffect = effect(() => { ...}); private readonly afterNextRenderRef = afterRender(() => { ... }); private readonly afterNextRender = afterNextRender(() => {...}); }
Бам! Такий підхід робить ваш код чистішим і організованішим.
⚙️ RxJS
А що робити з кодом, який написаний на стрімах?
Просто конвертуємо його в сигнали!
@Component({ selector: 'app-example', template: ` @if(pageData()) { {{ pageData() }} }@else { 'Empty data' } <button (click)="nextPage()">Next Page</button> `, }) export class ExampleComponent { // Створюємо BehaviorSubject як джерело даних private readonly page$ = new BehaviorSubject<number>(1); private readonly http = inject(HttpClient); // Приклад потоку даних private readonly data$ = of({ some: 'data' }); // Перетворюємо BehaviorSubject і купу логіки в сигнал readonly pageData = toSignal( this.page$.pipe( filter(Boolean), withLatestFrom(this.data$), filter(([_, data]) => !!data), map(([page, data]) => ({ page, data })), switchMap((params) => this.http.get('/api/data', { params }).pipe( catchError((err) => { console.error('Error fetching data:', err); return of(null); }) ) ) ) ); nextPage(): void { const currentPage = this.page$?.getValue() || 0; this.page$.next(currentPage + 1); } }
⚙️ Шаблони
Раніше ми використовували директиви на кшталт ngIf, ngFor, ng-template для роботи з умовними виразами та рендерингом шаблонів.
ngIf
// Раніше <div *ngIf="isVisible; else placeholder">Content is visible</div> <ng-template #placeholder> Placeholder </ng-template> // Зараз @if(isVisible) { Content is visible } @else { Placeholder }
ngFor
// Раніше <div *ngFor="let item of items"> <p>{{ item }}</p> </div> // Зараз @for (item of items; track item.name) { <li>{{ item.name }}</li> } @empty { <li>There are no items.</li> }
let
// Раніше <div *ngIf="user$ | async as user"> <h1>Hello, {{user.name}}</h1> <user-avatar [photo]="user.photo"/> <ul> <li *ngFor="let snack of user.favoriteSnacks; trackBy: trackById"> {{snack.name}} </li> </ul> <button (click)="update(user)">Update profile</button> </div> // Зараз @let user = user$ | async; @if(user) { <h1>Hello, {{user.name}}</h1> <user-avatar [photo]="user.photo"/> <ul> @for (snack of user.favoriteSnacks; track snack.id) { <li>{{snack.name}}</li> } </ul> <button (click)="update(user)">Update profile</button> }
defer
@Component({ selector: 'my-example', template: ` <h1>Some Title</h1> <div>Some description for many symbols</div> <!-- Відкладений рендеринг компонента --> @defer { <large-component></large-component> } `, }) export class ExampleComponent {}
⚙️ Зона відповідальності
Кожен компонент або сервіс повинен бути визначений у окремому файлі, щоб полегшити читання та обслуговування.
Файли повинні містити не більше 400 рядків коду для читабельності та уникнення складності.
Функції повинні бути невеликими й зазвичай не перевищувати 75 рядків.
⚙️ Неймінг
Class name. Назви класів повинні бути написані в стилі upper camel case (наприклад, MyComponent, UserService). Назва класу, компонента або сервісу повинна збігатися з назвою файлу, щоб полегшити навігацію проєктом (наприклад, клас UserService має бути в файлі user.service.ts).
File names. Для файлів потрібно використовувати прийняті суфікси, як-от: *.component.ts, *.directive.ts, *.module.ts, *.pipe.ts, *.service.ts тощо.
Component selectors. Використовуйте «dashed-case» або «kebab-case» для іменування селекторів компонентів.
Directives selectors. Використовуйте «lower camel case» для іменування селекторів директив.
Constants. Імена констант у форматі lower camel case (heroRoutes) легші для читання та розуміння порівняно з традиційними іменами в форматі UPPER_SNAKE_CASE (HERO_ROUTES).
Output. Не додавайте префікс до властивостей output. Називайте події без префікса «on».
⚙️ Послідовність і делегування
Розміщуйте властивості на початку, а потім методи. Розміщуйте приватні властивості після публічних, в алфавітному порядку.
Обмежте логіку в компоненті лише тією, що потрібна для відображення. Вся інша логіка має бути передана сервісам.
Залишайте компоненти простими та зосередженими на їх основному призначенні.
⚙️ Інше
Коли підписуєтесь на стріми, завжди переконуйтесь, що відписуєтесь від них належним чином, використовуючи оператори як-от take, takeUntil тощо.
Уникайте підписки всередині підписок.
Якщо можна виконувати їх усі паралельно, використовуйте forkJoin, або switchMap.
Уникайте використання типу any.
Використовуйте Lint з Husky для автоматичної перевірки коду перед кожним комітом.
Висновок
Просто дотримуйтесь цих порад, і буде вам щастя! 😄
13 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів