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 });
RTK is an optional peer dependency, but required to use the Redux specific entry point. Make sure you have it installed:
- npm
- yarn
- pnpm
- bun
npm install @reduxjs/toolkit
yarn add @reduxjs/toolkit
pnpm add @reduxjs/toolkit
bun add @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.
In order to set action.meta.undoable
, Redux Toolkit requires use of prepare callbacks.
adapter.withPayload
and adapter.withoutPayload
are provided for common cases.
- Default
- JSON Patches
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));
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<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
.
- Default
- JSON Patches
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());
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSlice } from "@reduxjs/toolkit"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<CounterState>({ limit: 10 }); const counterSlice = createSlice({ name: "counter", initialState: { counter: counterAdapter.getInitialState({ value: 0 }) }, reducers: { incremented: counterAdapter.undoableReducer( (draft) => { draft.value += 1; }, { selectPatchHistoryState: (state: NestedState) => state.counter, }, ), }, }); const { incremented } = counterSlice.actions; const store = makePrintStore(counterSlice.reducer); store.dispatch(incremented());
Other methods that update state (such as undo
, redo
, etc.) are valid case reducers and can be used with createSlice
.
- Default
- JSON Patches
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));
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSlice } from "@reduxjs/toolkit"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<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.
- Default
- JSON Patches
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));
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSlice } from "@reduxjs/toolkit"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<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.
- Default
- JSON Patches
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));
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSlice } from "@reduxjs/toolkit"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<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.
- Default
- JSON Patches
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()), });
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSlice } from "@reduxjs/toolkit"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<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 used to select the relevant slice of state.
- Default
- JSON Patches
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()), });
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSlice } from "@reduxjs/toolkit"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<CounterState>({ limit: 10 }); const initialState = { counter: counterAdapter.getInitialState({ value: 0 }) }; const counterSlice = createSlice({ name: "counter", initialState, reducers: { incremented: counterAdapter.undoableReducer( (draft) => { draft.value += 1; }, { selectPatchHistoryState: (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()), });