Skip to main content

Redux Methods

When imported from history-adapter/redux, the createHistoryAdapter function returns an object with additional methods for use with Redux Toolkit.

import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
value: number;
}

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

RTK is an optional peer dependency, but required to use the Redux specific entry point. Make sure you have it installed:


npm install @reduxjs/toolkit

All standard methods are available, plus the following:

undoableReducer

A wrapper around the undoable method that receives a case reducer and returns a wrapped reducer that manages undo and redo state.

Instead of providing an isUndoable callback, the reducer checks action.meta.undoable to determine if the action should be tracked.

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const counterSlice = createSlice({
  name: "counter",
  initialState: counterAdapter.getInitialState({ value: 0 }),
  reducers: {
    incremented: counterAdapter.undoableReducer((draft) => {
      draft.value += 1;
    }),
    incrementedBy: counterAdapter.undoableReducer(
      (draft, action: PayloadAction<number>) => {
        draft.value += action.payload;
      },
    ),
  },
});

const { incremented, incrementedBy } = counterSlice.actions;

const store = makePrintStore(counterSlice.reducer);

store.dispatch(incremented());

store.dispatch(incrementedBy(5));

It still accepts other configuration options, such as selectHistoryState.

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSlice } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const counterSlice = createSlice({
  name: "counter",
  initialState: { counter: counterAdapter.getInitialState({ value: 0 }) },
  reducers: {
    incremented: counterAdapter.undoableReducer(
      (draft) => {
        draft.value += 1;
      },
      {
        selectHistoryState: (state: NestedState) => state.counter,
      },
    ),
  },
});

const { incremented } = counterSlice.actions;

const store = makePrintStore(counterSlice.reducer);

store.dispatch(incremented());

Other methods

Other methods that update state (such as undo, redo, etc.) are valid case reducers and can be used with createSlice.

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSlice } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const counterSlice = createSlice({
  name: "counter",
  initialState: counterAdapter.getInitialState({ value: 0 }),
  reducers: {
    incremented: counterAdapter.undoableReducer((draft) => {
      draft.value += 1;
    }),
    undone: counterAdapter.undo,
    redone: counterAdapter.redo,
    jumped: counterAdapter.jump,
    paused: counterAdapter.pause,
    resumed: counterAdapter.resume,
    historyCleared: counterAdapter.clearHistory,
  },
});

const { incremented, undone, redone, jumped, paused, resumed, historyCleared } = counterSlice.actions;

const store = makePrintStore(counterSlice.reducer);

store.dispatch(incremented());

store.dispatch(undone());

store.dispatch(redone());

store.dispatch(jumped(-1));

withoutPayload

Creates a prepare callback which accepts a single (optional) argument, undoable. This is useful when you want to create an action that doesn't require a payload.

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSlice } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const counterSlice = createSlice({
  name: "counter",
  initialState: counterAdapter.getInitialState({ value: 0 }),
  reducers: {
    incremented: {
      prepare: counterAdapter.withoutPayload(),
      reducer: counterAdapter.undoableReducer((draft) => {
        draft.value += 1;
      }),
    },
  },
});

const { incremented } = counterSlice.actions;

const store = makePrintStore(counterSlice.reducer);

// undefined means the action is undoable
store.dispatch(incremented());
store.dispatch(incremented(false));
store.dispatch(incremented(true));

withPayload

Creates a prepare callback which accepts two arguments, payload (optional if potentially undefined) and undoable (optional). This is useful when you want to create an action that requires a payload.

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSlice } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const counterSlice = createSlice({
  name: "counter",
  initialState: counterAdapter.getInitialState({ value: 0 }),
  reducers: {
    incrementedBy: {
      prepare: counterAdapter.withPayload<number>(),
      reducer: counterAdapter.undoableReducer(
        (draft, action: PayloadAction<number>) => {
          draft.value += action.payload;
        },
      ),
    },
  },
});

const { incrementedBy } = counterSlice.actions;

const store = makePrintStore(counterSlice.reducer);

// undefined means the action is undoable
store.dispatch(incrementedBy(5));
store.dispatch(incrementedBy(5, false));
store.dispatch(incrementedBy(5, true));

getSelectors

Creates a set of useful selectors for the state managed by the adapter.

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSlice } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>({ limit: 10 });
const counterSlice = createSlice({
  name: "counter",
  initialState: counterAdapter.getInitialState({ value: 0 }),
  reducers: {
    incremented: counterAdapter.undoableReducer((draft) => {
      draft.value += 1;
    }),
  },
  selectors: {
    ...counterAdapter.getSelectors(),
  },
});

const { incremented } = counterSlice.actions;

const { selectCanUndo, selectCanRedo, selectPresent, selectPaused } =
  counterSlice.getSelectors();

const store = makePrintStore(counterSlice.reducer);

store.dispatch(incremented());

const print = getPrint();

print({
  canUndo: selectCanUndo(store.getState()), // true if there are past states
  canRedo: selectCanRedo(store.getState()), // true if there are future states
  present: selectPresent(store.getState()),
  paused: selectPaused(store.getState()),
});

An input selector can be provided if the state is nested, which will be combined using reselect's createSelector.

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSlice } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

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

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    incremented: counterAdapter.undoableReducer(
      (draft) => {
        draft.value += 1;
      },
      {
        selectHistoryState: (state: typeof initialState) => state.counter,
      },
    ),
  },
  selectors: {
    ...counterAdapter.getSelectors((state: typeof initialState) => state.counter),
  },
});

const { incremented } = counterSlice.actions;

const { selectCanUndo, selectCanRedo, selectPresent, selectPaused } =
  counterSlice.getSelectors();

const store = makePrintStore(counterSlice.reducer);

store.dispatch(incremented());

const print = getPrint();

print({
  canUndo: selectCanUndo(store.getState()), // true if there are past states
  canRedo: selectCanRedo(store.getState()), // true if there are future states
  present: selectPresent(store.getState()),
  paused: selectPaused(store.getState()),
});

The instance of createSelector used can be customised, and defaults to RTK's createDraftSafeSelector:

import "./styles.css";
import { makePrintStore } from "./reduxUtils";
import { getPrint } from "./utils";
import { createSelectorCreator, lruMemoize } from "reselect";
import { HistoryState } from "history-adapter";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

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

interface RootState {
  counter: HistoryState<CounterState>;
}

const { selectPresent } = counterAdapter.getSelectors(
  (state: RootState) => state.counter,
  { createSelector: createLruSelector },
);

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

const print = getPrint();

print({ initialState, counterState: selectPresent(initialState)  }) });