useHistoryState
React hook that tracks history of state
Example
import React from "react";
import useHistoryState from "@/hooks/useHistoryState";
export default function UseHistoryStateExample() {
const [state, setState, history] = useHistoryState<number>(0, 20);
const increment = React.useCallback(() => {
setState((prev: number) => prev + 1);
}, [setState]);
const decrement = React.useCallback(() => {
setState((prev: number) => prev - 1);
}, [setState]);
const revert = React.useCallback(() => {
history.pop();
}, [history]);
const clear = React.useCallback(() => {
history.clearItems();
}, [history]);
return (
<div className="flex w-full items-start justify-center gap-8 p-4 text-ctp-text">
<section className="flex flex-col items-center justify-center gap-4 p-4">
<p className="text-lg">Counter</p>
<div className="flex gap-4">
<button className="text-ctp-green" onClick={increment}>
up
</button>
<p>{state}</p>
<button className="text-ctp-red" onClick={decrement}>
down
</button>
</div>
<div className="flex gap-4">
<button className="text-ctp-blue" onClick={revert}>
revert
</button>
<button className="text-ctp-pink" onClick={clear}>
clear
</button>
</div>
</section>
<section>
<h1>Histories</h1>
<ol>
{history.histories.map((value) => (
<li key={value}>{value}</li>
))}
</ol>
</section>
</div>
);
}
Code
import { useState, useRef, useCallback, useEffect } from "react";
type SetStateCallback<T> = (state: T) => T;
export interface History<T> {
histories: T[];
pop: () => T | null;
clearItems: () => void;
deleteItem: (value: T) => void;
}
export type InitialStateCallback<T> = () => T;
export type SetState<T> = (nextState: T | SetStateCallback<T>) => void;
export type UseHistoryState<T = unknown> = (
initialState?: T | InitialStateCallback<T>,
size?: number,
) => [T, SetState<T>, History<T>];
/**
* React hook that tracks history of state
* @param {T | (() => T)} initialState - initial state
* @param {number} size - max size of history
* @returns {[T, SetState<T>, History<T>]} state, setState, history, clearItems, deleteItem
*/
export default function useHistoryState<T>(
initialState: T | (() => T),
size: number = 20,
): [T, SetState<T>, History<T>] {
const [state, setState] = useState<T>(initialState);
const [_, forceUpdate] = useState(0);
const stateRef = useRef<T>(state);
const historyRef = useRef<T[]>([]);
const historyPop = useCallback(() => {
const historyLength = historyRef.current.length;
if (historyLength > 0) {
const value = historyRef.current.pop()!;
setState(value);
return value;
}
return null;
}, []);
const historyDelete = useCallback(
(value: T) => {
const deletedHistories = historyRef.current.filter(
(item) => item !== value,
);
historyRef.current = deletedHistories;
setState(deletedHistories[deletedHistories.length - 1]);
},
[historyRef, setState],
);
const historyClear = useCallback(() => {
historyRef.current = [];
forceUpdate((prev) => prev + 1);
setState(initialState)
}, [historyRef, forceUpdate]);
const setStateCallback = useCallback(
(nextValue: any) => {
const value =
typeof nextValue === "function"
? nextValue(stateRef.current)
: nextValue;
if (typeof stateRef.current !== "undefined") {
historyRef.current.push(stateRef.current);
}
setState(value);
},
[historyRef, setState],
);
useEffect(() => {
if (typeof state !== "undefined") {
stateRef.current = state;
}
}, [state]);
useEffect(() => {
const historyLength = historyRef.current.length;
if (historyLength > size) {
const excess = historyLength - size;
historyRef.current.splice(0, excess);
}
}, [size, state]);
return [
state,
setStateCallback,
{
histories: historyRef.current,
pop: historyPop,
deleteItem: historyDelete,
clearItems: historyClear,
},
];
}