Undo / Redo
Ctrl+Z/Ctrl+Y history for forms with useFormHistory
Overview
No form library offers built-in undo/redo. useFormHistory() fills that gap with debounced snapshots, keyboard shortcuts, and an optional visual controls component.
import { HistoryControls, useFormHistory } from '@letar/forms'
function ProductEditor() {
const form = useDeclarativeForm()
const history = useFormHistory(form)
return (
<Form schema={ProductSchema} onSubmit={save}>
<HistoryControls history={history} />
<Form.Field.String name="title" />
<Form.Field.RichText name="description" />
<Form.Field.Currency name="price" />
<Form.Button.Submit>Save</Form.Button.Submit>
</Form>
)
}Full example
The complete sandbox example, read directly from the form-develop-app source at build time.
'use client'
import { Box, Code, Heading, Text, VStack } from '@chakra-ui/react'
import { DemoPageLayout } from '../_components'
export default function UndoRedoDemoPage() {
return (
<DemoPageLayout title="useFormHistory — Undo/Redo" description="Ctrl+Z/Ctrl+Y для форм с историей изменений">
<VStack gap={8} align="stretch">
<Box>
<Heading size="md" mb={3}>
Использование
</Heading>
<Code display="block" whiteSpace="pre" fontSize="xs" p={4} borderRadius="md">
{`import { useFormHistory, HistoryControls } from '@letar/forms'
function ProductForm() {
const form = useDeclarativeForm()
const history = useFormHistory(form, {
maxHistory: 50, // Макс. снапшотов в стеке
debounceMs: 500, // Пауза перед записью (мс)
keyboard: true, // Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z
persist: false, // Сохранять в sessionStorage
})
return (
<Form schema={ProductSchema} onSubmit={save}>
{/* Визуальные кнопки Undo/Redo */}
<HistoryControls history={history} showCounter />
<Form.Field.String name="title" />
<Form.Field.RichText name="description" />
<Form.Field.Currency name="price" />
<Form.Button.Submit>Сохранить</Form.Button.Submit>
</Form>
)
}`}
</Code>
</Box>
<Box>
<Heading size="md" mb={3}>
API
</Heading>
<Code display="block" whiteSpace="pre" fontSize="xs" p={4} borderRadius="md">
{`const {
undo, // () => void — отменить
redo, // () => void — повторить
canUndo, // boolean
canRedo, // boolean
currentIndex, // number — текущая позиция
historyLength, // number — всего снапшотов
clear, // () => void — очистить историю
history, // HistoryEntry[] — для отладки
} = useFormHistory(form, config?)`}
</Code>
</Box>
<Box>
<Heading size="md" mb={3}>
Как работает
</Heading>
<VStack align="start" gap={2} fontSize="sm">
<Text>
1. Хук подписывается на <strong>form.store</strong> (TanStack Form reactive store)
</Text>
<Text>
2. При каждом изменении — debounce 500ms, затем <strong>structuredClone</strong> снапшот
</Text>
<Text>
3. Undo/Redo применяет снапшот через <strong>form.setFieldValue</strong> для каждого поля
</Text>
<Text>
4. Изменения от undo/redo <strong>не записываются</strong> в историю (isUndoRedoRef)
</Text>
<Text>5. При ветвлении (undo → изменение) — будущие записи обрезаются</Text>
</VStack>
</Box>
<Box>
<Heading size="md" mb={3}>
Keyboard Shortcuts
</Heading>
<VStack align="start" gap={2} fontSize="sm">
<Text>
<strong>Ctrl+Z</strong> (Cmd+Z на Mac) — Undo
</Text>
<Text>
<strong>Ctrl+Shift+Z</strong> или <strong>Ctrl+Y</strong> — Redo
</Text>
<Text>
Шорткаты включены по умолчанию (<Code>keyboard: true</Code>)
</Text>
</VStack>
</Box>
<Box>
<Heading size="md" mb={3}>
Когда использовать
</Heading>
<VStack align="start" gap={2} fontSize="sm">
<Text>- CMS / редакторы контента (много полей, частые правки)</Text>
<Text>- Конфигураторы товаров (изменение параметров)</Text>
<Text>- Формы с RichText (пользователь привык к Ctrl+Z)</Text>
<Text>- Длинные формы (>10 полей) где легко ошибиться</Text>
</VStack>
</Box>
</VStack>
</DemoPageLayout>
)
}shadcn-вариант этого примера ещё не готов.
Vue-пример для этого поля появится позже.
Angular-пример для этого поля появится позже.
How It Works
- Subscribes to
form.store(TanStack Form reactive store) - On change → debounce 500ms →
structuredClonesnapshot - Undo/Redo applies snapshot via
form.setFieldValueper field - Undo/redo changes are not recorded in history (prevents recursion)
- Branching (undo → edit) trims future entries
API: useFormHistory(form, config?)
const history = useFormHistory(form, {
maxHistory: 50, // Max snapshots (default: 50)
debounceMs: 500, // Delay before recording (default: 500ms)
keyboard: true, // Ctrl+Z/Y shortcuts (default: true)
persist: false, // sessionStorage (default: false)
persistKey: 'form-history',
})Returns:
| Field | Type | Description |
|---|---|---|
undo | () => void | Undo last change |
redo | () => void | Redo undone change |
canUndo | boolean | Has undo history |
canRedo | boolean | Has redo history |
currentIndex | number | Position in stack |
historyLength | number | Total snapshots |
clear | () => void | Reset history |
Keyboard Shortcuts
| Keys | Action |
|---|---|
Ctrl+Z / Cmd+Z | Undo |
Ctrl+Shift+Z / Ctrl+Y | Redo |
Disable: keyboard: false.
HistoryControls
<HistoryControls
history={historyApi}
showCounter // Show "3/7" counter
size="sm" // xs | sm | md
/>When to Use
- CMS / content editors (many fields, frequent edits)
- Product configurators
- Forms with RichText (users expect Ctrl+Z)
- Long forms (10+ fields) where mistakes are costly