Angular IDPStrong Junior
🔥 0

Модуль 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, — і далі шаблон просто відображає.