Managed value history
Import createHistoryMiddleware from form-please/history when users need to
navigate earlier editable values. History is optional and does not create
another live form store. React Hook Form remains the owner of current values and
form state.
Configure one history feature
Create the feature outside the component, put that exact reference in the
form's fixed middleware list, and pass both values to useHistory:
const historyFeature = createHistoryMiddleware({ limit: 50 })
export function HistoryPreview() {
const form = nativeFormKit.useForm(historyDefinition, {
defaultValues: { name: "Ada Lovelace", projects: [] },
middleware: [historyFeature],
})
const history = useHistory(form, historyFeature)
const { snapshot } = historyThe returned handle includes a reactive snapshot with canUndo, canRedo,
index, and length. It also keeps the underlying getSnapshot() and
subscribe() external-store operations.
One feature can serve several forms. Each form can configure only one history
feature. useHistory(form, feature) and the lower-level feature.handle(form)
reject a form that does not use that exact feature.
Know what history records
History records successful managed value updates from generated controls,
generated array actions, and form.update. It retains complete independent
schema-input snapshots rather than patches or events.
Initial values, native reset, direct form.api mutations, and
application-owned useFieldArray operations bypass history. When raw values
diverge, the next managed update or history operation makes those current
values a new non-undoable boundary and discards the older branches.
History never retains errors, touched state, validation, submission, focus, context, or the default-value baseline. Restore keeps the current RHF metadata, recalculates dirty state against the original defaults, and may regenerate private field-array row IDs.
Group and retain positions
Consecutive control edits to one path share a group for 750 milliseconds by
default. Another source, another path, or an expired window starts a group. Set
groupWindow: 0 to make every managed update separately undoable.
limit defaults to 100 retained groups. Set another non-negative integer or
Infinity. A limit of zero retains only the current position.
Navigate through middleware
undo(), redo(), seek(index), and import(value) return promises. Their
managed transaction source has type: "history" and an undo, redo, seek,
or import action. beforeUpdate and middleware can cancel or transform the
target, and afterUpdate observes the final commit.
Operations report "applied", "unavailable", "cancelled", or
"transformed". A transformed restore becomes a new group and removes its
redo branch. A post-commit failure rejects the operation without rolling back
the committed values or history position.
Export and import an in-memory journal
Export from one handle and import into another handle:
export async function copyHistoryJournal(
source: HistoryHandle<HistoryInput>,
target: HistoryHandle<HistoryInput>,
): Promise<HistoryOperationResult> {
const journal = source.export()
return target.import(journal)
}HistoryJournal<Input> version 1 contains complete input entries and a numeric
current index. Import validates its protocol, index, entry roots, and configured
retention limit. It does not require Standard Schema success because editable
history can contain temporarily invalid values.
The journal is an in-memory navigation artifact, not a JSON persistence format
or audit log. Date and RegExp leaves are detached. Browser-owned values such
as File and Blob preserve their identity. Applications own transport,
serialization, storage, and trust boundaries.
Try the complete History workflow.