Vize

use-history

Record bounded undo/redo history over the writes of a ref.

Package @vizejs/composable/use-history
Own the source vize lib pull composable:use-history
Runtime exports useHistory
Gzip budget 2560 B

Minimal setup

Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).

vp install @vizejs/composable

Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.

import { useHistory } from "@vizejs/composable/use-history";

Try it

Edit and undo a release title

Give a text editor bounded undo/redo and group a multi-write operation into one undo step.

Open the interactive example

What to observe: Edit the title and try Undo and Redo. Apply publication title performs its two assignments as one step. A new edit after Undo discards redo history; Clear history retains the title.

Each ref assignment is recorded synchronously. The example keeps at most ten undo entries. Object mutations require reassignment and an appropriate clone function. Its watcher and retained history follow the component scope.

Compare the initial and interacted states
Edit and undo a release title: initial state
Initial state
Edit and undo a release title: state after the verified interactions
After the verified interactions

Copy the example

Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.

<script setup lang="ts">
import { ref, useId } from "vue";
import { useHistory } from "@vizejs/composable/use-history";

const title = ref("Draft release notes");
const titleId = useId();
const { canUndo, canRedo, undoCount, redoCount, undo, redo, batch, clear } = useHistory(title, {
  capacity: 10,
});
function publishTitle(): void {
  batch(() => {
    title.value = title.value.trim();
    title.value = "Vize release notes";
  });
}
</script>

<template>
  <div class="composable-example">
    <label :for="titleId">Release title</label>
    <input :id="titleId" v-model="title" />
    <div class="example-actions">
      <button type="button" :disabled="!canUndo" @click="() => undo()">Undo</button>
      <button type="button" :disabled="!canRedo" @click="() => redo()">Redo</button>
      <button type="button" @click="publishTitle">Apply publication title</button>
      <button type="button" @click="() => clear()">Clear history</button>
    </div>
    <output aria-live="polite"
      >{{ undoCount }} undo {{ undoCount === 1 ? "step" : "steps" }} · {{ redoCount }} redo
      {{ redoCount === 1 ? "step" : "steps" }}</output
    >
  </div>
</template>

Runtime contract

Utility Category Stability SSR Hydration Cleanup Targets Host globals Uses
useHistory state experimental safe stable reactive-scope web, server, worker, native, desktop, terminal — tryOnScopeDispose

API

useHistory

Record bounded undo/redo history over the writes of a ref. Recording is shallow and identity-based, matching Vue's own change detection: assignments to source.value are recorded (observed with flush: "sync", so every synchronous write counts), writes that are Object.is-equal to the current value are not changes, and in-place mutations of object values are invisible — pair mutable values with UseHistoryOptions.clone and reassign. Undoing and redoing restore values through clone as well, so snapshots never share identity with the live value unless the default identity clone is kept. When a user-provided clone throws, the failed operation leaves history unchanged and the error propagates. Safe during server rendering: no browser globals are read and no timers start. Cleanup rule: when the owning reactive scope stops, recording stops and every retained snapshot is released, so undo/redo return false afterwards; call inside an active scope, or the watcher lives as long as the source.

function useHistory<Value>( source: Ref<Value>, options: UseHistoryOptions<Value> = {}, ): HistoryControls
const text = shallowRef("");
const { undo, redo, batch } = useHistory(text);
text.value = "a";
batch(() => {
  text.value = "ab";
  text.value = "abc";
});
undo(); // text.value === "a" (the batch is one step)
redo(); // text.value === "abc"

Types

UseHistoryOptions

Options for useHistory.

Member Type Description
capacity? number Maximum number of undo entries retained; recording a change beyond it drops the oldest entry. The redo stack is bounded by construction, since redo entries only ever come from undone changes. Must be an integer greater than zero and is fixed at creation.
clone? (value: Value) => Value Clone applied to every value captured into history and to every value restored out of it, isolating snapshots from later in-place mutation.

HistoryControls

Reactive undo/redo controls returned by useHistory.

Member Type Description
canUndo ComputedRef<boolean> Whether HistoryControls.undo currently has an entry to restore.
canRedo ComputedRef<boolean> Whether HistoryControls.redo currently has an entry to restore.
undoCount ComputedRef<number> Number of retained undo entries.
redoCount ComputedRef<number> Number of retained redo entries.
undo () => boolean Restore the newest undo entry and move the current value onto the redo stack. The restoring write is not recorded.
redo () => boolean Restore the newest redo entry and move the current value back onto the undo stack. The restoring write is not recorded.
batch <Result>(run: () => Result) => Result Group every source write inside run into at most one undo entry. The entry restores the value from just before the batch. It is committed only when the final value differs (Object.is) from the starting value, and it is committed even when run throws, so a partially applied batch stays undoable as one step. Nested calls collapse into the outermost batch. The callback's return value is passed through.
clear () => void Drop every undo and redo entry while keeping the current value.