Как добавить валидацию для предотвращения ошибок при многократном использовании undo?
Что проверяют
Понимаете ли вы, что ошибки undo редко связаны с пустым стеком. Настоящие проблемы в рассинхроне истории и состояния: отмена поверх незавершённого запроса, повтор после нового действия, команда, которую уже нельзя применить. Следом спрашивают, как это работает с сервером или при совместном редактировании.
Как отвечать
Сначала модель истории, потом правила, которые её защищают.
- модель: два стека
pastиfutureили массив с указателем. В них лежат снимки состояния (просто, но дорого по памяти) или команды с методамиdoиundo(паттерн Command). Средний вариант это патчи: Immer черезproduceWithPatchesотдаёт прямые и обратные патчи на каждое изменение; - доступность:
canUndoиcanRedoвычисляются из истории, по ним отключаются кнопки и горячие клавиши. Проверка внутриundo()остаётся, но пользователь до неё не доходит; - атомарность: указатель сдвигается только после успешного применения. Если
undo()бросил исключение, история остаётся прежней, иначе она разойдётся с экраном; - предусловия команды: перед отменой команда проверяет, что её объект существует и находится в ожидаемом состоянии. Элемент мог удалить другой пользователь;
- новое действие: очищает
future, иначе redo вернёт состояние из другой ветки; - асинхронность: пока идёт сохранение, отмены встают в очередь. При зажатом
Ctrl+Zсобытияkeydownидут сериями сevent.repeat, и каждая отмена ждёт предыдущую; - лимит и группировка: длина истории ограничена, а ввод текста склеивается в одну запись, чтобы отмена не стирала по букве.
Из готового: zundo для Zustand, redux-undo для Redux, Y.UndoManager для совместного редактирования на Yjs.
Где спотыкаются
Ограничиваются проверкой if (index < 0). Она защищает от пустого стека, но не от команды, которую уже нельзя применить к текущему состоянию.
Хранят в истории ссылки на изменяемые объекты. Следующая мутация тихо переписывает сохранённые снимки, и отмена возвращает не то.
Забывают про сервер: отменяют локально, а запрос на изменение уже ушёл. Без обратной операции на бэкенде интерфейс и база расходятся.