/** * History Hook for Undo/Redo functionality * Manages a history stack of states with configurable max length */ import { useState, useCallback, useRef } from 'react'; interface UseHistoryOptions { maxHistory?: number; } interface UseHistoryReturn { /** Current state value */ state: T; /** Update state and add to history */ setState: (newState: T | ((prev: T) => T)) => void; /** Set state without adding to history (for batch updates) */ setStateWithoutHistory: (newState: T | ((prev: T) => T)) => void; /** Undo to previous state */ undo: () => void; /** Redo to next state */ redo: () => void; /** Can undo (has previous history) */ canUndo: boolean; /** Can redo (has forward history) */ canRedo: boolean; /** Clear all history */ clearHistory: () => void; /** Reset state and clear history */ reset: (initialState: T) => void; /** Number of undo steps available */ undoCount: number; /** Number of redo steps available */ redoCount: number; } export function useHistory( initialState: T, options: UseHistoryOptions = {} ): UseHistoryReturn { const { maxHistory = 50 } = options; // Current state const [state, setStateInternal] = useState(initialState); // History stacks const pastRef = useRef([]); const futureRef = useRef([]); // Track if we're in the middle of an undo/redo to avoid adding to history const isUndoRedoRef = useRef(false); // Update state and add to history const setState = useCallback((newState: T | ((prev: T) => T)) => { setStateInternal((prev) => { const next = typeof newState === 'function' ? (newState as (prev: T) => T)(prev) : newState; // Don't add to history if we're undoing/redoing or if state hasn't changed if (!isUndoRedoRef.current && JSON.stringify(prev) !== JSON.stringify(next)) { // Add current state to past pastRef.current = [...pastRef.current, prev].slice(-maxHistory); // Clear future when new change is made futureRef.current = []; } return next; }); }, [maxHistory]); // Set state without adding to history (useful for programmatic resets) const setStateWithoutHistory = useCallback((newState: T | ((prev: T) => T)) => { isUndoRedoRef.current = true; setStateInternal((prev) => { const next = typeof newState === 'function' ? (newState as (prev: T) => T)(prev) : newState; return next; }); // Reset flag after state update setTimeout(() => { isUndoRedoRef.current = false; }, 0); }, []); // Undo to previous state const undo = useCallback(() => { if (pastRef.current.length === 0) return; isUndoRedoRef.current = true; setStateInternal((current) => { const previous = pastRef.current[pastRef.current.length - 1]; pastRef.current = pastRef.current.slice(0, -1); futureRef.current = [current, ...futureRef.current]; return previous; }); // Reset flag after state update setTimeout(() => { isUndoRedoRef.current = false; }, 0); }, []); // Redo to next state const redo = useCallback(() => { if (futureRef.current.length === 0) return; isUndoRedoRef.current = true; setStateInternal((current) => { const next = futureRef.current[0]; futureRef.current = futureRef.current.slice(1); pastRef.current = [...pastRef.current, current]; return next; }); // Reset flag after state update setTimeout(() => { isUndoRedoRef.current = false; }, 0); }, []); // Clear all history const clearHistory = useCallback(() => { pastRef.current = []; futureRef.current = []; }, []); // Reset state and clear history const reset = useCallback((initialState: T) => { setStateInternal(initialState); pastRef.current = []; futureRef.current = []; }, []); return { state, setState, setStateWithoutHistory, undo, redo, canUndo: pastRef.current.length > 0, canRedo: futureRef.current.length > 0, clearHistory, reset, undoCount: pastRef.current.length, redoCount: futureRef.current.length, }; } export default useHistory;