Skip to main content

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:

  1. state: The current value of the state.
  2. push(newValue): A function to update the state.
    • Call this function with the newValue for the state.
    • This updates the state, adds newValue to the history, and resets the pointer to this new state.
    • If you call push after performing one or more undo operations, any subsequent ("redo") states in the history are discarded.
    • It respects the capacity limit.
  3. undo(): A function to move the state back to the previous value in the history.
    • Returns true if the operation was successful (i.e., there was a previous state).
    • Returns false if already at the initial state (nothing to undo).
  4. redo(): A function to move the state forward to the next value in the history (only possible after an undo).
    • Returns true if the operation was successful (i.e., there was a state that had been previously undone).
    • Returns false if already at the most recent state (nothing to redo).

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;