State Management with History
This document explores managing state with undo/redo capabilities in React using a custom hook.
It allows a component to manage a piece of state while keeping a record of its previous values. Users can then navigate back (undo) and forward (redo) through this history.
Hook Documentation & Usage
API
useStateWithHistory(initialState, capacity?)
Parameters
initialState: The initial value for the state. Can be any JavaScript type.capacity(Optional,number, default:10): The maximum number of state snapshots to keep in the history. When a new state is added and the history is full, the oldest state is removed.
Return Value
Returns an array containing four elements:
state: The current value of the state.push(newValue): A function to update the state.- Call this function with the
newValuefor the state. - This updates the
state, addsnewValueto the history, and resets the pointer to this new state. - If you call
pushafter performing one or moreundooperations, any subsequent ("redo") states in the history are discarded. - It respects the
capacitylimit.
- Call this function with the
undo(): A function to move the state back to the previous value in the history.- Returns
trueif the operation was successful (i.e., there was a previous state). - Returns
falseif already at the initial state (nothing to undo).
- Returns
redo(): A function to move the state forward to the next value in the history (only possible after anundo).- Returns
trueif the operation was successful (i.e., there was a state that had been previously undone). - Returns
falseif already at the most recent state (nothing to redo).
- Returns
The useStateWithHistory Hook Code
hooks/useStateWithHistory.js
import { useState, useRef, useCallback } from 'react';
/**
* A custom React hook that manages state with undo/redo functionality.
* It keeps a history of state changes up to a specified capacity.
*
* @param {*} initialState - The initial value of the state.
* @param {number} [capacity=10] - The maximum number of history entries to store. Oldest entries are discarded when capacity is reached.
* @returns {[*, Function, Function, Function]} An array containing:
* - `state`: The current state value.
* - `push(value: *)`: A function to update the state. This adds the new value to history, potentially clearing redo history and respecting capacity.
* - `undo(): boolean`: A function to revert to the previous state in history. Returns `true` if successful, `false` otherwise.
* - `redo(): boolean`: A function to advance to the next state in history (after an undo). Returns `true` if successful, `false` otherwise.
*/
function useStateWithHistory(initialState, capacity = 10) {
const [state, setState] = useState(initialState);
const history = useRef([initialState]);
const pointer = useRef(0);
const push = useCallback((value) => {
// Discard future history if we branched off after an undo
const newHistory = history.current.slice(0, pointer.current + 1);
// Limit capacity
if (newHistory.length >= capacity) {
newHistory.shift(); // Remove the oldest entry
}
// Add new state
history.current = [...newHistory, value];
// Move pointer to the new state
pointer.current = history.current.length - 1;
// Update the actual state
setState(value);
}, [capacity]); // Dependency: capacity ensures push is updated if capacity changes
const undo = useCallback(() => {
if (pointer.current > 0) {
pointer.current--;
setState(history.current[pointer.current]);
return true;
}
return false;
}, []); // No dependencies needed as refs don't trigger updates
const redo = useCallback(() => {
if (pointer.current < history.current.length - 1) {
pointer.current++;
setState(history.current[pointer.current]);
return true;
}
return false;
}, []); // No dependencies needed
return [state, push, undo, redo];
}
export default useStateWithHistory;
Example Usage
ExampleComponent.js
import React from 'react';
// Assuming the hook is saved in './hooks/useStateWithHistory.js'
// and exported as default
import useStateWithHistory from './hooks/useStateWithHistory';
function CounterComponent() {
// Manage count state with default history capacity (10)
const [count, setCount, undoCount, redoCount] = useStateWithHistory(0);
// Note: For disabling buttons accurately, the hook could be modified
// to return boolean flags like `canUndo` and `canRedo`.
// This example omits robust disabling logic for simplicity.
return (
<div>
<h2>Counter with History</h2>
<p>Current Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)} style={{ marginLeft: '5px' }}>Decrement</button>
<div style={{ marginTop: '10px' }}>
<button onClick={undoCount}>Undo</button>
<button onClick={redoCount} style={{ marginLeft: '5px' }}>Redo</button>
</div>
</div>
);
}
export default CounterComponent;