На экране оплаты подписки на закрытый Telegram-канал статус обновляется автоматически. Есть и кнопка «Проверить оплату». Пользователь нажимает её, пока фоновый запрос ещё выполняется. Интерфейс показывает «Оплачено», а через полсекунды возвращается к «Ожидаем оплату». В Network у обоих ответов HTTP 200: ошибка видна только в данных и порядке их прихода.
Фоновый запрос прочитал заказ до оплаты, но его ответ задержался. Ручная проверка прочитала заказ уже после оплаты и ответила первой. Если обработчик вызывает setSnapshot(next) для каждого ответа, версия 7 перезаписывает уже показанную версию 8.
Какой ответ считать свежим
Время отправки запроса, момент чтения в базе и порядок прихода ответов могут расходиться. Например, последний отправленный запрос может попасть на отстающую реплику и вернуть более старый статус. Поэтому сравниваем версии данных, которые прочитал сервер.
Эндпоинт возвращает orderId, revision, payment и access. В примере revision — положительное целое число в безопасном диапазоне JavaScript. Сервер хранит статус и его версию вместе: при принятом изменении платежа или доступа обновляет поля и увеличивает версию атомарно. Один и тот же номер версии в рамках заказа всегда означает один и тот же снимок. Если исходные данные приходят из разных систем, такой снимок можно хранить в отдельной таблице для экрана заказа.
Версия защищает от устаревших ответов на чтение. Она не исправляет ситуацию, в которой сам сервер применил запоздалое платёжное событие и присвоил неверному состоянию новый номер. На бэкенде всё равно нужно проверять, допустим ли переход статуса, и не применять один платёж дважды.
Оплата и доступ меняются не одновременно. На paid / pending показываем «Платёж получен, готовим доступ». Кнопка входа появляется на paid / ready и скрывается после подтверждённого возврата, даже если отзыв доступа ещё выполняется. Сам редирект с платёжной страницы не меняет эти статусы. Скрытие кнопки тоже не отзывает доступ: это отдельная операция на сервере.
Правило обновления состояния
Функция сравнивает входящий снимок с уже принятым. В ней перечислены только статусы учебного API; у реального эндпоинта этот список должен совпадать с его схемой.
function mergeSnapshot(orderId, current, next) { if ( next == null || next.orderId !== orderId || !Number.isSafeInteger(next.revision) || next.revision < 1 || !["awaiting_payment", "paid", "refunded"].includes(next.payment) || !["pending", "ready", "revoked"].includes(next.access) ) { return current; } if (current && next.revision <= current.revision) return current; return next;}
После проверки HTTP-ответа и разбора JSON оба обработчика вызывают setSnapshot(current => mergeSnapshot(orderId, current, next)). React передаёт актуальное значение из очереди обновлений, поэтому два почти одновременных ответа не сравниваются с одним и тем же устаревшим snapshot. Повреждённый ответ полезно зафиксировать на границе API как ошибку контракта; последнее корректное состояние при этом сохраняется.
Если экран переключается между заказами, задайте компоненту key={orderId}: заказ B начнёт с собственного состояния, даже если последняя версия заказа A была 99, а у B пока только версия 1. В очистке эффекта останавливайте таймер и отменяйте или игнорируйте незавершённые запросы опроса. Для запросов к одному заказу проверка версий по-прежнему нужна. Ошибка опроса хранится отдельно: можно показать «Не удалось обновить статус», сохранив уже полученное «Оплачено».
Что проверить на стенде
|
Порядок прихода ответов |
Что останется на экране |
|---|---|
|
|
v8: оплата получена, доступ ещё готовится |
|
|
Тот же снимок; повтор ничего не меняет |
|
|
v9: появилась кнопка входа |
|
|
v10: возврат, доступ закрыт |
|
|
v10: возврат, кнопка скрыта; отзыв доступа ещё не подтверждён |
|
На экране заказ B, пришёл заказ A, v99 |
Снимок заказа B не меняется |
|
После v8 сервер вернул |
v8 сохраняется |
Чтобы воспроизвести ошибку, настройте мок-сервер: первый GET возвращает awaiting_payment с версией 7 через 800 мс, второй — paid с версией 8 через 50 мс. Запустите второй запрос, пока первый ещё в работе. С прямым setSnapshot(next) экран сначала покажет версию 8, потом версию 7. После правки он останется на версии 8. Затем верните paid / ready с версией 9: кнопка должна появиться. Так тест проверит и защиту от отката, и нормальное обновление.
Если API не отдаёт версию, сначала стоит выяснить, можно ли читать состояние из одного согласованного источника и держать не более одного запроса в работе. Это уберёт часть гонок. Но последовательный опрос сам по себе не защитит от отстающей реплики, а cache: "no-store" касается HTTP-кеша, а не актуальности данных в базе.
Источники: React: обновление состояния по предыдущему значению, React: загрузка данных и очистка эффекта, React: сброс состояния с помощью ключа.
ссылка на оригинал статьи https://habr.com/ru/articles/1105620/