Модуль 7 · урок 1 з 10
Чотири стани екрана
Більшість екранів малюють один стан — той, де все добре й дані є. Насправді станів чотири, і три з них зʼявляються частіше, ніж хочеться. Цей урок про те, як перестати забувати решту три.
Чотири стани
Пріоритет має значення
@if (loading()) {
<app-skeleton />
} @else if (error(); as message) {
<app-error [message]="message" (retry)="reload()" />
} @else if (rows().length === 0) {
<app-empty [filtered]="hasFilters()" />
} @else {
<app-table [rows]="rows()" />
}
Порядок саме такий, і він не довільний:
- Завантаження першим. Поки дані йдуть, ми ще не знаємо, буде помилка чи порожньо.
- Помилка перед порожнім. Якщо запит упав, список порожній — але це не значить, що даних немає. Показати «нічого не знайдено» замість помилки — брехня.
- Порожньо перед даними. Очевидно, але саме тут забувають розрізняти два види порожнечі.
Показати «Нічого не знайдено», коли насправді впав запит. Користувач думає, що даних немає,
і йде створювати дублікат уже наявного запису. Один @else if (error())
перед перевіркою на порожнечу знімає цілий клас таких проблем.
Стани й повторне завантаження
// ❌ при кожному оновленні екран блимає скелетоном
reload(): void {
this.loading.set(true);
this.rows.set([]); // ← ось це зайве
this.api.list().subscribe(…);
}
// ✅ дані лишаються на екрані, згори показуємо тонкий індикатор
reload(): void {
this.refreshing.set(true);
this.api.list().pipe(
finalize(() => this.refreshing.set(false)),
).subscribe(rows => this.rows.set(rows));
}
Різниця між першим завантаженням і оновленням — те, що відрізняє акуратний інтерфейс від смикливого. Скелетон доречний, коли показувати ще нічого. Коли дані вже є, достатньо тонкої смужки прогресу вгорі або легкого затемнення таблиці.
readonly firstLoad = computed(() => this.loading() && this.rows().length === 0);
readonly refreshing = computed(() => this.loading() && this.rows().length > 0);
Стан як одне значення
// замість трьох незалежних прапорців — розрізнюваний union із модуля 2
type ScreenState<T> =
| { kind: 'loading' }
| { kind: 'error'; message: string }
| { kind: 'empty'; filtered: boolean }
| { kind: 'data'; rows: readonly T[] };
@switch (state().kind) {
@case ('loading') { <app-skeleton /> }
@case ('error') { <app-error [message]="state().message" /> }
@case ('empty') { <app-empty [filtered]="state().filtered" /> }
@case ('data') { <app-table [rows]="state().rows" /> }
}
Перевага очевидна: неможливо опинитись у стані «одночасно завантаження й помилка».
Пріоритет описується один раз — у функції, яка будує state, — і далі
шаблон просто відображає.