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
- bun
npm install @reduxjs/toolkit
yarn 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.
- 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 combined using reselect
's createSelector
.
- 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()), });
The instance of createSelector
used can be customised, and defaults to RTK's createDraftSafeSelector
:
- Default
- JSON Patches
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) }) });
import "./styles.css"; import { makePrintStore } from "./reduxUtils"; import { getPrint } from "./utils"; import { createSelectorCreator, lruMemoize } from "reselect"; import { PatchHistoryState } from "history-adapter"; import { createPatchHistoryAdapter } from "history-adapter/redux"; interface CounterState { value: number; } const counterAdapter = createPatchHistoryAdapter<CounterState>({ limit: 10 }); const createLruSelector = createSelectorCreator(lruMemoize); interface RootState { counter: PatchHistoryState<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) }) });