Skip to content

history

Tracks state history and enables undo / redo / reset. Every state change, whether dispatched through reducers or made via set, is recorded as a snapshot.

Basic usage

ts
import { withPlugins } from "@kin-store/core";
import { history } from "@kin-store/plugins";

const store = withPlugins({ count: 0 })
  .use({
    reducers: {
      increment: (state, n: number) => ({ count: state.count + n }),
    },
  })
  .use("history", history());

store.dispatch.increment(1); // count = 1
store.dispatch.increment(1); // count = 2

store.history.canUndo(); // true
store.history.undo(); // count = 1
store.history.redo(); // count = 2
store.history.reset(); // count = 0

Plugin methods

MethodDescription
canUndo()Returns true if there is a past state to undo to
canRedo()Returns true if there is a future state to redo to
undo()Moves back one step; returns true if moved, false if already at start
redo()Moves forward one step; returns true if moved, false if already at end
reset()Restores the baseline state and clear the history
rebase()Makes the current state the new undo floor, discarding prior history

Options

OptionTypeDefaultDescription
limitnumberInfinityMax snapshots to keep. When exceeded, the oldest is dropped.
ts
.use('history', history({ limit: 50 }))

Composing with persist

After async hydration, call rebase() so undo and reset don't step back to the pre-hydration state:

ts
const store = withPlugins({ items: [] as string[] })
  .use("persist", persist({ key: "items" }))
  .use("history", history())
  .use({ reducers: { add: (s, t: string) => ({ items: [...s.items, t] }) } });

await store.persist.hydrationComplete();
store.history.rebase();

Inside middleware

The history plugin uses an internal _restore reducer to change state, so every undo and redo travels through the middleware pipeline. A logging middleware will see it:

ts
middleware: () => (ctx, next) => {
  // Includes "history._restore", assuming the plugin is registered under
  // the "history" namespace.
  console.log(ctx.reducer.name);
  return next();
},

A guard middleware can cancel it:

ts
middleware: () => (ctx, next) => {
  if (ctx.reducer.name === 'history._restore' && isLocked) return CANCELED;
  return next();
},

Released under the MIT License