Skip to main content

Standard Methods

This page documents the standard methods available when using a history adapter instance.

Adapter setup
import { createHistoryAdapter } from "history-adapter";

interface CounterState {
value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });

getInitialState

Receives an initial state value and returns a "history state" shape.

import "./styles.css";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const initialState = counterAdapter.getInitialState({ value: 0 });

print({ initialState })

tip

A standalone version of this method is available as getInitialState.

import "./styles.css";
import { getInitialState } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();
const initialState = getInitialState({ value: 0 });

print({ initialState })

undoable

Wraps an immer recipe to automatically manage undo and redo state. Because immer wraps the state in a draft, you can safely mutate the state directly.

import "./styles.css";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const increment = counterAdapter.undoable((draft) => {
  draft.value += 1;
});

const initialState = counterAdapter.getInitialState({ value: 0 });

const nextState = increment(initialState);

print({ initialState, nextState })

State operators

All of the methods to update state (undo, redo, etc.) will act mutably when passed a draft, otherwise return a new state. The same applies to the function returned by undoable.

import "./styles.css";
import { produce } from "immer";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const increment = counterAdapter.undoable((draft) => {
  draft.value += 1;
});

const initialState = counterAdapter.getInitialState({ value: 0 });

const nextState = increment(initialState); // creates a new state

const withDraft = produce(initialState, (draft) => {
  increment(draft); // called as a mutator
});

print({
  initialState: initialState.present.value,
  nextState: nextState.present.value,
  withDraft: withDraft.present.value,
})

Extracting whether a change is undoable

By default, a change is assumed to be undoable, and is added to the history stack. To have finer control over this behaviour, you can pass an isUndoable function as part of the optional configuration object. This function receives the same arguments as the recipe, and should return a boolean (or undefined, in which case the change is assumed to be undoable).

import "./styles.css";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const incrementBy = counterAdapter.undoable(
  (draft, amount: number) => {
    draft.value += amount;
  },
  {
    // don't bother adding to history if the amount is zero
    isUndoable: (amount) => amount !== 0,
  },
);

const initialState = counterAdapter.getInitialState({ value: 0 });

const incrementedByZero = incrementBy(initialState, 0);

const incrementedByOne = incrementBy(incrementedByZero, 1);

print({ incrementedByZero, incrementedByOne })

Nested history state

Sometimes the state you want to manage is nested within a larger object. In this case, you can pass a selectHistoryState function to undoable to extract the relevant state.

import "./styles.css";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
interface RootState {
  counter: HistoryState<CounterState>;
}

const increment = counterAdapter.undoable(
  (draft) => {
    draft.value += 1;
  },
  {
    selectHistoryState: (state: RootState) => state.counter,
  },
);

const initialState = { counter: counterAdapter.getInitialState({ value: 0 }) };

const nextState = increment(initialState);

print({ initialState, nextState })

It should be a function that receives the wider state object and returns the history state object.

undo, redo, jump

These methods allow you to navigate the history stack.

import "./styles.css";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const initialState = counterAdapter.getInitialState({ value: 0 });

const increment = counterAdapter.undoable((draft) => {
  draft.value += 1;
});

const nextState = increment(initialState);

const undoneState = counterAdapter.undo(nextState);

const redoneState = counterAdapter.redo(undoneState);

// negative numbers move back, positive move forward
const jumpedState = counterAdapter.jump(redoneState, -1);

print({ nextState, undoneState, redoneState, jumpedState })

clearHistory

Clears the history stack, while preserving the present state.

import "./styles.css";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const initialState = counterAdapter.getInitialState({ value: 0 });

const increment = counterAdapter.undoable((draft) => {
  draft.value += 1;
});

const nextState = increment(initialState);

const clearedState = counterAdapter.clearHistory(nextState);

print({ nextState, clearedState })

Pausing history

To pause recording of history, you can set the paused flag in state to true - pause and resume methods are provided for this purpose.

Changes made while paused will not be added to the history stack, but will still update the present state.

import "./styles.css";
import { createHistoryAdapter } from "history-adapter";
import { getPrint } from "./utils";
const print = getPrint();

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const initialState = counterAdapter.getInitialState({ value: 0 });

const increment = counterAdapter.undoable((draft) => {
  draft.value += 1;
});

const pausedState = counterAdapter.pause(initialState);

const whilePaused = increment(pausedState);

const resumedState = counterAdapter.resume(whilePaused);

const undone = counterAdapter.undo(resumedState);

print({ initialState, pausedState, whilePaused, resumedState, undone })