В современных интерфейсах на базе Stimulus асинхронные HTTP-запросы используются постоянно: автодополнение, подгрузка данных при прокрутке, фильтрация, формы с мгновенной валидацией. Основная сложность заключается в том, что пользовательские действия могут происходить быстрее, чем завершается сетевой запрос. В результате в приложение могут поступать устаревшие ответы, приводящие к неконсистентному состоянию интерфейса.
Типичный пример — поле поиска с автозапросами. Каждый ввод символа инициирует новый запрос, но предыдущие ещё не завершены. Без механизма отмены последний ответ может прийти раньше предыдущего, либо наоборот, что приведёт к отображению неверных данных.
Современный JavaScript предоставляет стандартный механизм отмены
асинхронных операций — AbortController. Он интегрирован с
fetch и рядом других API.
this.controller = new AbortController()
fetch(url, { signal: this.controller.signal })
Вызов this.controller.abort() прерывает запрос и
выбрасывает исключение AbortError. Это исключение должно
обрабатываться явно, иначе оно будет восприниматься как ошибка
выполнения.
В контексте Stimulus контроллера AbortController обычно
хранится как часть состояния экземпляра.
Stimulus предоставляет чёткий жизненный цикл: connect,
disconnect, обработчики событий. Это позволяет аккуратно
управлять асинхронными операциями.
Ключевое правило: каждый контроллер отвечает за отмену собственных запросов при уничтожении или замене.
disconnect() {
if (this.controller) {
this.controller.abort()
}
}
Такой подход предотвращает обновление DOM после удаления элемента и утечки памяти.
При инициировании нового запроса предыдущий должен быть отменён. Типовой паттерн:
load() {
if (this.controller) {
this.controller.abort()
}
this.controller = new AbortController()
fetch(this.urlValue, { signal: this.controller.signal })
.then(r => r.json())
.then(data => this.update(data))
.catch(error => {
if (error.name !== "AbortError") {
throw error
}
})
}
Таким образом обеспечивается строгое соответствие: один активный запрос — одно актуальное состояние.
Stimulus не навязывает систему управления состоянием, но поощряет локальное состояние внутри контроллера. Для асинхронных операций это особенно важно.
Типичные состояния:
loading)loaded)error)empty)Состояние удобно хранить в свойствах экземпляра:
this.loading = true
this.error = null
Или в виде значений Stimulus (static values), если
требуется реактивность и доступ из шаблона.
DOM не должен обновляться напрямую из асинхронного кода без проверки
актуальности состояния. Даже при использовании
AbortController возможны ситуации, когда логика усложняется
дополнительными условиями.
Практика — проверять, что контроллер всё ещё активен:
if (this.controller.signal.aborted) return
Это особенно важно при цепочках then, таймерах или
комбинации fetch и setTimeout.
Ошибка запроса не всегда означает ошибку интерфейса. Отмена запроса — ожидаемое поведение и не должна переводить контроллер в состояние ошибки.
catch(error) {
if (error.name === "AbortError") return
this.error = error
this.showError()
}
Таким образом сохраняется предсказуемость: пользовательские действия не приводят к ложным ошибкам.
В сложных сценариях используется не только отмена, но и идентификация запросов. Например, когда отмена невозможна (сторонний API) или запросы выполняются параллельно.
this.requestId = crypto.randomUUID()
const currentId = this.requestId
fetch(...)
.then(data => {
if (this.requestId !== currentId) return
this.update(data)
})
Этот приём дополняет AbortController и защищает от гонок
состояний.
Состояние загрузки удобно отражать через CSS-классы и targets:
static targets = ["spinner", "content"]
set loading(value) {
this.spinnerTarget.hidden = !value
this.contentTarget.hidden = value
}
Асинхронный код изменяет только состояние, а визуальное представление остаётся декларативным.
При повторном подключении контроллера (например, при навигации Turbo) состояние должно быть инициализировано заново. Нельзя полагаться на прошлые запросы или значения.
connect() {
this.controller = null
this.loading = false
this.error = null
}
Это гарантирует воспроизводимость поведения независимо от истории взаимодействий.
При росте сложности интерфейса каждый Stimulus-контроллер должен
оставаться автономным. Управление отменой запросов и состоянием не
выносится в глобальные объекты без необходимости. Вместо этого
используется композиция: несколько контроллеров, каждый со своим циклом
запросов и собственным AbortController.
Такой подход сохраняет основное преимущество Stimulus — простоту и предсказуемость поведения, даже при активной работе с асинхронными данными.