Svelte
Асинхронные данные и реактивность
Виджет погоды на дашборде грузит данные из API при выборе города. Пока запрос летит, нужен спиннер. Если запрос упал, нужно сообщение об ошибке и кнопка повтора. Когда данные пришли, из них считается ещё и совет: брать ли зонт. Тянуть для этого тяжёлую библиотеку загрузки данных избыточно, когда виджет один. Руны Svelte 5 покрывают этот сценарий тремя реактивными значениями и одним эффектом, без внешних зависимостей.
- Автодополнение поиска: каждый ввод запускает запрос, прошлые результаты сбрасываются, показывается состояние загрузки
- Виджеты дашборда: каждый блок грузит свои данные и сам управляет своими состояниями загрузки и ошибки
- Подгрузка деталей по выбору: выбор элемента в списке тянет его детали с показом спиннера в панели
- Формы с проверкой на сервере: асинхронная валидация поля при вводе с индикатором и сообщением об ошибке
Предварительные знания
- Понимание производных значений через руну `$derived`
- Знание эффектов через руну `$effect` и того, когда они перезапускаются
- Базовое знание fetch и промисов: запрос, ожидание ответа, обработка ошибки
Складываем асинхронные данные в реактивное состояние
Промис сам по себе не реактивен: его результат приходит позже, и интерфейс должен на это отреагировать. Приём состоит в том, чтобы завести реактивное состояние через `$state` и записать в него результат, когда промис разрешится. Запрос запускают в `$effect`, который повторно выполняется при изменении своих зависимостей, например выбранного идентификатора. Так смена входа автоматически перезагружает данные.
Эффект читает city, поэтому city становится его зависимостью. При смене города эффект перезапускается и тянет новые данные. Когда промис разрешается, запись в weather обновляет реактивное состояние, и разметка перерисовывается. Промежуточное значение null до прихода данных и даёт простейший индикатор загрузки в этом примере.
Асинхронный коллбэк внутри `$effect` нельзя превращать в async-функцию самого эффекта: функция очистки эффекта должна возвращаться синхронно. Запрос запускают внутри тела эффекта, а результат записывают в состояние в then или после await во вложенной функции, не делая сам колбэк эффекта асинхронным.
В `$effect` читается city и запускается fetch по этому городу. Что произойдёт, когда пользователь сменит город?
Паттерн loading и error на трёх полях
Полноценный асинхронный виджет различает три состояния: идёт загрузка, пришла ошибка, пришли данные. Их моделируют тремя реактивными полями: data, loading и error. Перед запросом ставят loading в true и сбрасывают error. В случае успеха пишут data и снимают loading. В случае сбоя пишут error и тоже снимают loading. Разметка выбирает, что показать, по этим трём полям.
Порядок веток важен: сначала проверяют loading, затем error, затем данные. Сброс error перед каждым запросом убирает сообщение о прошлой неудаче при повторной попытке. Блок finally гарантирует, что loading снимется и при успехе, и при ошибке. Это полноценная модель состояний без какой-либо внешней библиотеки, на чистых рунах.
| Поле | Тип | Роль |
|---|---|---|
| data | T | null | Полученные данные или null до их прихода |
| loading | boolean | Идёт ли сейчас запрос |
| error | string | null | Сообщение об ошибке или null, если ошибки нет |
Для одного-двух виджетов трёх полей достаточно. Тяжёлые библиотеки загрузки данных оправданы, когда нужны кеширование между компонентами, дедупликация одинаковых запросов, фоновое обновление и инвалидация. Для локального асинхронного состояния руны покрывают задачу без лишнего веса.
Зачем перед каждым новым запросом сбрасывать error в null, прежде чем ставить loading в true?
Производные от данных и гонка устаревших ответов
Когда данные лежат в реактивном состоянии, производные от них считаются обычным `$derived`. Совет брать зонт это функция от полученной погоды, и он пересчитывается сам, как только weather обновится. Производное не нужно вручную синхронизировать с загрузкой: оно зависит от data, и приход данных автоматически запускает его пересчёт.
Отдельная проблема асинхронных данных это гонка устаревших ответов. Пользователь быстро переключает города: запрос за первым городом может вернуться позже запроса за вторым и перезаписать актуальные данные старыми. Лекарство это пометить каждый запрос признаком актуальности и игнорировать ответ, если вход успел смениться. Функция очистки эффекта помогает отменить или обесценить устаревший запрос.
При смене city эффект сначала вызывает функцию очистки прошлого запуска, выставляя cancelled в true для старого запроса, и только потом запускается заново. Когда поздний ответ старого запроса всё же придёт, он увидит cancelled и не запишет устаревшие данные. Так последним в weather всегда оказывается ответ на актуальный запрос, а не тот, что просто вернулся последним по времени.
Без защиты от гонки интерфейс эпизодически показывает данные не для того входа, что выбран сейчас. Баг плавающий и зависит от сетевых задержек, поэтому на локальной быстрой сети он почти не виден, а в проде проявляется. Флаг отмены в функции очистки эффекта это минимальная и достаточная защита.
Пользователь быстро переключил город с Москвы на Берлин. Запрос по Москве из-за задержки сети вернулся позже запроса по Берлину. Как флаг cancelled в функции очистки эффекта предотвращает показ устаревших данных?
Связь с другими темами
Этот урок про асинхронные данные на клиенте. Он перекликается со стримингом и сторами:
- `$derived` — Совет из погоды это производное от полученных данных через `$derived`
- Стриминг из load — Тот же приём каркаса и поздних данных, но на сервере через возврат промиса из load
- Stores и интероп — Сторы дают альтернативный способ обернуть асинхронный поток данных
Итог
- Асинхронные данные складывают в реактивное состояние через `$state`: отдельные поля под данные, состояние загрузки и ошибку
- Запрос запускают в `$effect`, который перезапускается при изменении входа (например, выбранного города), и грузит свежие данные
- Производные значения от полученных данных считают через `$derived`, и они пересчитываются, когда данные приходят
- Паттерн loading и error строится на трёх полях состояния и не требует тяжёлой библиотеки для простых случаев
- Гонку устаревших ответов гасят проверкой актуальности запроса, чтобы поздний ответ не перезаписал свежий
Связанные уроки
- sv-07-derived — Производные от полученных данных строятся на `$derived`, поэтому понимание производных значений обязательно
- sv-29-streaming — Стриминг из load и клиентская загрузка решают одну задачу раннего каркаса и поздних данных, но в разных средах
- sv-32-stores-interop — Сторы исторически удобны для асинхронных потоков, что даёт альтернативный взгляд на ту же задачу