Skip to main content

Setup

A "history adapter" for managing undoable (and redoable) state changes with immer, which pairs well with state management solutions like Redux and Zustand.

Also includes a Redux specific version, with additional utilities for use with Redux Toolkit.

Sandboxes

Redux Toolkit

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter({ limit: 5 });

const { selectPresent, ...historySelectors } = counterAdapter.getSelectors();

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

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    incrementedBy: counterAdapter.undoableReducer(
      (state, action: PayloadAction<number>) => {
        state.value += action.payload;
      }
    ),
    undone: counterAdapter.undo,
    redone: counterAdapter.redo,
    pauseToggled(state) {
      state.paused = !state.paused;
    },
    historyCleared: counterAdapter.clearHistory,
    reset: () => initialState,
  },
  selectors: {
    ...historySelectors,
    selectCount: (state) => selectPresent(state).value,
  },
});

export const {
  incrementedBy,
  undone,
  redone,
  pauseToggled,
  historyCleared,
  reset,
} = counterSlice.actions;

export const { selectCount, selectCanUndo, selectCanRedo, selectPaused } =
  counterSlice.selectors;

Zustand

import { create } from "zustand";
import { createHistoryAdapter, HistoryState } from "history-adapter";

interface CounterState {
  value: 0;
}

interface RootState extends HistoryState<CounterState> {
  incrementBy: (by: number) => void;
  undo: () => void;
  redo: () => void;
  togglePause: () => void;
  clearHistory: () => void;
  reset: () => void;
}

const counterAdapter = createHistoryAdapter<CounterState>();

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

export const useCounterStore = create<RootState>()((set) => ({
  ...initialState,
  incrementBy: (by) =>
    set(
      counterAdapter.undoable((state) => {
        state.value += by;
      })
    ),
  undo: () => set(counterAdapter.undo),
  redo: () => set(counterAdapter.redo),
  togglePause: () => set((prev) => ({ paused: !prev.paused })),
  clearHistory: () => set(counterAdapter.clearHistory),
  reset: () => set(initialState),
}));

Installation

Install with your package manager of choice:


npm install history-adapter

Usage

The main export of history-adapter is the createHistoryAdapter function. This function takes an optional configuration object and returns an object with useful methods for managing undoable state changes.

For a list of all available methods, see the API reference.

By default, history entries will be a copy of state before/after each change. However, you can use createPatchHistoryAdapter to store JSON Patches instead.

Configuration

The createHistoryAdapter function accepts an optional configuration object with some of the following properties:

  • limit (number): The maximum number of history entries to store.
    • If not provided, all history entries will be stored.