Appearance
useStateHistory
Track the change history of a state automatically — every change commits a history record — also provides undo and redo functionality
Demo
Usage
tsx
import { useStateHistory } from '@reause/core'
import { useState } from 'react'
const [count, setCount] = useState(0)
const { history, undo, redo, canUndo, canRedo } = useStateHistory({
value: count,
onChange: setCount,
})
setCount(1) // every change commits a history record
console.log(history)
/* [
{ snapshot: 1, timestamp: 1601912898062 },
{ snapshot: 0, timestamp: 1601912898061 }
] */
undo() // count back to the previous record
redo() // count forward againInternally, an effect is used to trigger a history point when the state is modified. This means that history points are triggered asynchronously batching modifications in the same "tick".
You can use undo to reset the state to the last history point.
Objects / arrays
When working with objects or arrays, since changing their attributes does not change the reference, it will not trigger the committing. React state is normally replaced instead of mutated — the clone option and custom dump / parse support mutation-style sources and create clones for each history record:
tsx
import { useStateHistory } from '@reause/core'
import { useState } from 'react'
const [target, setTarget] = useState({ foo: 1, bar: 2 })
const { history, setSource } = useStateHistory([target, setTarget], { clone: true })
setSource({ foo: 2, bar: 2 }) // committed immediatelyCustom Clone Function
useStateHistory only embeds the minimal clone function x => JSON.parse(JSON.stringify(x)). To use a full featured or custom clone function, you can set up via the clone options.
For example, using structuredClone:
tsx
import { useStateHistory } from '@reause/core'
const stateHistory = useStateHistory([target, setTarget], { clone: structuredClone })Or by using lodash's cloneDeep:
tsx
import { useStateHistory } from '@reause/core'
import { cloneDeep } from 'lodash-es'
const stateHistory = useStateHistory([target, setTarget], { clone: cloneDeep })Or a more lightweight klona:
tsx
import { useStateHistory } from '@reause/core'
import { klona } from 'klona'
const stateHistory = useStateHistory([target, setTarget], { clone: klona })Custom Dump and Parse Function
Instead of using the clone options, you can pass custom functions to control the serialization and parsing. In case you do not need history values to be objects, this can save an extra clone when undoing. It is also useful in case you want to have the snapshots already stringified to be saved to local storage for example.
tsx
import { useStateHistory } from '@reause/core'
const stateHistory = useStateHistory([target, setTarget], {
dump: JSON.stringify,
parse: JSON.parse,
})History Capacity
We will keep all the history by default (unlimited) until you explicitly clear them up, you can set the maximal amount of history to be kept by capacity options.
tsx
const { history, clear } = useStateHistory([target, setTarget], {
capacity: 15, // limit to 15 history records
})
clear() // explicitly clear all the historyHistory WatchOptionFlush Timing
Multiple state updates in the same tick render once and collapse into a single commit carrying the final value; there is no per-assignment flush: 'sync' timing. You can use commit() in case you need to create multiple history points in the same "tick"
tsx
import { useStateHistory } from '@reause/core'
import { useState } from 'react'
const [r, setR] = useState(0)
const { history, commit, setSource } = useStateHistory([r, setR])
setSource(1)
commit()
setSource(2)
commit()
console.log(history)
/* [
{ snapshot: 2 },
{ snapshot: 1 },
{ snapshot: 0 },
] */On the other hand, you can use batch(fn) to generate a single history point for several operations
tsx
import { useStateHistory } from '@reause/core'
import { useState } from 'react'
const [r, setR] = useState({ names: [], version: 1 })
const { history, batch, setSource } = useStateHistory([r, setR])
batch(() => {
setSource(current => ({ names: [...current.names, 'Lena'], version: current.version + 1 }))
})
console.log(history)
/* [
{ snapshot: { names: [ 'Lena' ], version: 2 },
{ snapshot: { names: [], version: 1 },
] */Recommended Readings
Type Declarations
Toggle
ts
export interface UseStateHistoryOptions<Raw, Serialized = Raw> {
capacity?: number;
clone?: boolean | ((value: Raw) => Raw);
dump?: (value: Raw) => Serialized;
parse?: (value: Serialized) => Raw;
shouldCommit?: (oldValue: Raw, newValue: Raw) => boolean;
}
export interface UseStateHistoryControls<Raw, Serialized = Raw> {
source: Raw;
last: UseRefHistoryRecord<Serialized>;
undoStack: UseRefHistoryRecord<Serialized>[];
redoStack: UseRefHistoryRecord<Serialized>[];
canUndo: boolean;
canRedo: boolean;
isTracking: boolean;
setSource: Dispatch<SetStateAction<Raw>>;
commit: () => void;
clear: () => void;
reset: () => void;
pause: () => void;
resume: (commitNow?: boolean) => void;
batch: (fn: (cancel: () => void) => void) => void;
}
export interface UseStateHistoryReturn<Raw, Serialized = Raw> extends UseStateHistoryControls<Raw, Serialized> {
history: UseRefHistoryRecord<Serialized>[];
undo: () => void;
redo: () => void;
}
export interface UseRefHistoryRecord<T> {
snapshot: T;
timestamp: number;
}
export interface UseStateManualHistoryControls<Raw, Serialized = Raw> {
source: Raw;
last: UseRefHistoryRecord<Serialized>;
undoStack: UseRefHistoryRecord<Serialized>[];
redoStack: UseRefHistoryRecord<Serialized>[];
canUndo: boolean;
canRedo: boolean;
undo: () => void;
redo: () => void;
clear: () => void;
reset: () => void;
setSource: Dispatch<SetStateAction<Raw>>;
}
export interface UseStateManualHistoryReturn<Raw, Serialized = Raw> extends UseStateManualHistoryControls<Raw, Serialized> {
history: UseRefHistoryRecord<Serialized>[];
commit: () => void;
}
export function useStateHistory<Raw, Serialized = Raw>(state: State<Raw>, options?: UseStateHistoryOptions<Raw, Serialized>): UseStateHistoryReturn<Raw, Serialized>