Загрузочные состояния и индикаторы прогресса

Загрузочные состояния отражают период между инициацией действия и получением результата. В контексте Stimulus это особенно важно: фреймворк часто используется для постепенного обогащения HTML, асинхронных запросов, отправки форм и динамического обновления частей страницы. Отсутствие явной индикации загрузки приводит к ощущению «зависшего» интерфейса, даже если логика работает корректно.

Stimulus не навязывает готовых компонентов для индикаторов прогресса, но предоставляет достаточный набор механизмов для их реализации: контроллеры, targets, values, lifecycle-хуки и работу с событиями.


Базовый принцип: состояние как часть контроллера

Загрузочное состояние удобно рассматривать как внутреннее состояние контроллера. Чаще всего оно выражается булевым значением:

export default class extends Controller {
  static values = { loading: Boolean }
}

Такое значение становится реактивным: при изменении loadingValue автоматически вызывается метод loadingValueChanged. Это позволяет централизованно управлять отображением индикаторов.

loadingValueChanged(value) {
  if (value) {
    this.showLoader()
  } else {
    this.hideLoader()
  }
}

Ключевая идея — разделение логики:

  • изменение состояния происходит в обработчиках событий и асинхронных операций;
  • отображение привязано к реакции на изменение значения.

Использование targets для индикаторов загрузки

Targets обеспечивают декларативную связь между DOM и контроллером. Для индикаторов обычно используются отдельные элементы:

<div data-controller="request">
  <button data-action="click->request#submit">Отправить</button>
  <div data-request-target="loader" hidden>
    Загрузка…
  </div>
</div>
export default class extends Controller {
  static targets = ["loader"]
  static values = { loading: Boolean }

  showLoader() {
    this.loaderTarget.hidden = false
  }

  hideLoader() {
    this.loaderTarget.hidden = true
  }
}

Такой подход:

  • исключает поиск элементов через querySelector;
  • упрощает поддержку структуры;
  • делает разметку самодокументируемой.

Интеграция с асинхронными запросами

Наиболее распространённый сценарий — загрузка данных через fetch. Загрузочное состояние устанавливается до запроса и сбрасывается после завершения, независимо от результата.

async submit() {
  this.loadingValue = true

  try {
    const response = await fetch("/api/data")
    const data = await response.json()
    this.handleSuccess(data)
  } catch (error) {
    this.handleError(error)
  } finally {
    this.loadingValue = false
  }
}

Блок finally критически важен: он гарантирует корректное завершение состояния даже при ошибках сети или исключениях в коде.


Блокировка интерфейса во время загрузки

Помимо визуального индикатора, часто требуется запретить повторные действия. Это достигается тем же состоянием загрузки.

static targets = ["button"]

loadingValueChanged(value) {
  this.buttonTarget.disabled = value
  this.loaderTarget.hidden = !value
}

Такое поведение:

  • предотвращает дублирующие запросы;
  • устраняет гонки состояний;
  • делает поведение интерфейса предсказуемым.

Прогресс вместо простого индикатора

Для длительных операций индикатора «Загрузка…» недостаточно. В таких случаях используется прогресс-бар. Stimulus не управляет прогрессом напрямую, но легко связывается с источником данных о ходе выполнения.

<div data-controller="upload">
  <progress max="100" data-upload-target="progress"></progress>
</div>
export default class extends Controller {
  static targets = ["progress"]

  updateProgress(percent) {
    this.progressTarget.value = percent
  }
}

Обновление может происходить:

  • из обработчика XMLHttpRequest.upload.onprogress;
  • через события WebSocket;
  • из периодических запросов к серверу.

Stimulus в этом случае выполняет роль слоя синхронизации между источником данных и DOM.


Управление CSS-состояниями через классы

Иногда индикатор реализуется исключительно через CSS-анимацию. Вместо показа/скрытия элементов используется переключение классов.

loadingValueChanged(value) {
  this.element.classList.toggle("is-loading", value)
}
.is-loading {
  opacity: 0.6;
  pointer-events: none;
}

.is-loading::after {
  content: "";
  /* стили спиннера */
}

Этот подход особенно удобен для:

  • кнопок с встроенным спиннером;
  • карточек и секций;
  • полноэкранных оверлеев.

Загрузочные состояния и жизненный цикл контроллера

Lifecycle-методы Stimulus позволяют автоматически управлять индикаторами при подключении и отключении контроллера.

connect() {
  this.loadingValue = false
}

disconnect() {
  this.cleanup()
}

Это важно при использовании:

  • динамической подгрузки HTML;
  • Turbo Frames и Turbo Streams;
  • условного рендеринга элементов.

Контроллер не должен оставлять активное состояние после удаления из DOM.


Работа с глобальными загрузками

Иногда требуется единый индикатор для всего приложения. Stimulus решает эту задачу через отдельный контроллер, реагирующий на события.

document.addEventListener("request:start", () => {
  this.loadingValue = true
})

document.addEventListener("request:end", () => {
  this.loadingValue = false
})

Другие контроллеры генерируют эти события:

this.dispatch("start")

Такой паттерн:

  • устраняет жёсткие зависимости;
  • поддерживает слабую связанность компонентов;
  • позволяет масштабировать логику без усложнения кода.

Синхронизация с Turbo

При использовании Turbo навигации и форм можно реагировать на стандартные события:

  • turbo:submit-start
  • turbo:submit-end
  • turbo:before-fetch-request
  • turbo:before-fetch-response
connect() {
  this.element.addEventListener("turbo:submit-start", () => {
    this.loadingValue = true
  })

  this.element.addEventListener("turbo:submit-end", () => {
    this.loadingValue = false
  })
}

Это позволяет внедрять индикаторы загрузки без вмешательства в серверную логику.


Типичные ошибки при реализации загрузочных состояний

Смешивание логики и отображения Манипуляции DOM непосредственно в методах запросов затрудняют поддержку и повторное использование.

Отсутствие обработки ошибок Индикатор, который не скрывается при исключении, блокирует интерфейс.

Глобальные флаги без контекста Единое состояние загрузки для всех операций приводит к ложным срабатываниям.

Зависимость от таймеров Индикаторы, скрывающиеся по setTimeout, не отражают реальное состояние системы.


Архитектурный эффект

Загрузочные состояния в Stimulus — не декоративный элемент, а часть архитектуры взаимодействия. Чёткое разделение:

  • состояния,
  • источника асинхронных событий,
  • отображения в DOM

превращает индикаторы прогресса в предсказуемый и расширяемый механизм, который одинаково хорошо работает как для локальных компонентов, так и для всего приложения.