React Stately状态历史:撤销重做功能实现

React Stately状态历史:撤销重做功能实现

【免费下载链接】react-spectrum 一系列帮助您构建适应性强、可访问性好、健壮性高的用户体验的库和工具。 【免费下载链接】react-spectrum 项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum

痛点:为什么需要状态历史管理?

在现代Web应用中,用户经常需要进行复杂的操作,比如表单编辑、数据配置、图形绘制等。一旦操作失误,如果没有撤销重做功能,用户可能需要重新开始整个流程,这极大地影响了用户体验和工作效率。

你还在为这些场景头疼吗?

  • 用户误删重要数据无法恢复
  • 复杂的多步骤操作无法回退
  • 需要实现类似Photoshop的历史记录面板
  • 状态管理混乱,难以维护操作历史

本文将带你深入React Stately的状态管理机制,实现专业的撤销重做功能,让你的应用具备完整的历史操作能力。

React Stately状态管理基础

React Stately是Adobe React Spectrum生态中的状态管理库,提供跨平台的状态管理解决方案。其核心是useControlledState Hook,为状态历史管理奠定了坚实基础。

useControlledState核心机制

function useControlledState<T, C = T>(
  value: T, 
  defaultValue: T, 
  onChange?: (v: C, ...args: any[]) => void
): [T, (value: T, ...args: any[]) => void]

这个Hook提供了受控和非受控状态的双重支持,正是构建状态历史系统的理想基础。

撤销重做系统架构设计

系统架构图

mermaid

核心数据结构

interface HistoryState<T> {
  past: T[];          // 过去的状态历史
  present: T;         // 当前状态
  future: T[];        // 未来的状态(重做栈)
  limit: number;      // 历史记录限制
}

interface HistoryActions {
  undo: () => void;
  redo: () => void;
  clear: () => void;
  canUndo: boolean;
  canRedo: boolean;
}

完整实现方案

1. 基础历史管理Hook

import { useCallback, useRef, useState } from 'react';

function useHistoryState<T>(initialState: T, limit: number = 50) {
  const [state, setState] = useState<HistoryState<T>>({
    past: [],
    present: initialState,
    future: [],
    limit
  });

  const canUndo = state.past.length > 0;
  const canRedo = state.future.length > 0;

  const updateState = useCallback((newState: T) => {
    setState(prev => {
      const newPast = [...prev.past, prev.present];
      // 限制历史记录数量
      if (newPast.length > prev.limit) {
        newPast.shift();
      }
      
      return {
        past: newPast,
        present: newState,
        future: [], // 新的操作会清空重做栈
        limit: prev.limit
      };
    });
  }, []);

  const undo = useCallback(() => {
    if (!canUndo) return;
    
    setState(prev => {
      const previous = prev.past[prev.past.length - 1];
      const newPast = prev.past.slice(0, -1);
      const newFuture = [prev.present, ...prev.future];
      
      return {
        past: newPast,
        present: previous,
        future: newFuture,
        limit: prev.limit
      };
    });
  }, [canUndo]);

  const redo = useCallback(() => {
    if (!canRedo) return;
    
    setState(prev => {
      const next = prev.future[0];
      const newFuture = prev.future.slice(1);
      const newPast = [...prev.past, prev.present];
      
      return {
        past: newPast,
        present: next,
        future: newFuture,
        limit: prev.limit
      };
    });
  }, [canRedo]);

  const clear = useCallback(() => {
    setState({
      past: [],
      present: initialState,
      future: [],
      limit
    });
  }, [initialState, limit]);

  return {
    state: state.present,
    setState: updateState,
    undo,
    redo,
    clear,
    canUndo,
    canRedo,
    history: state
  };
}

2. 与React Stately集成

function useControlledHistoryState<T>(
  value: T | undefined,
  defaultValue: T,
  onChange?: (newValue: T) => void,
  limit: number = 50
) {
  const history = useHistoryState(value ?? defaultValue, limit);
  
  const setValue = useCallback((newValue: T) => {
    history.setState(newValue);
    onChange?.(newValue);
  }, [history, onChange]);

  return {
    value: history.state,
    setValue,
    undo: history.undo,
    redo: history.redo,
    clear: history.clear,
    canUndo: history.canUndo,
    canRedo: history.canRedo
  };
}

3. 类型安全的增强版本

interface UseHistoryOptions<T> {
  limit?: number;
  equalityFn?: (a: T, b: T) => boolean;
  onStateChange?: (newState: T, action: 'undo' | 'redo' | 'set') => void;
}

function useEnhancedHistoryState<T>(
  initialState: T,
  options: UseHistoryOptions<T> = {}
) {
  const { limit = 50, equalityFn = Object.is, onStateChange } = options;
  
  const [state, setState] = useState<HistoryState<T>>({
    past: [],
    present: initialState,
    future: [],
    limit
  });

  const updateState = useCallback((newState: T) => {
    if (equalityFn(state.present, newState)) {
      return; // 状态相同,不记录历史
    }

    setState(prev => {
      const newPast = [...prev.past, prev.present];
      if (newPast.length > prev.limit) {
        newPast.shift();
      }
      
      const newHistory = {
        past: newPast,
        present: newState,
        future: [],
        limit: prev.limit
      };
      
      onStateChange?.(newState, 'set');
      return newHistory;
    });
  }, [equalityFn, onStateChange, state.present]);

  // 省略undo/redo实现(与基础版本类似)
  
  return {
    state: state.present,
    setState: updateState,
    // ...其他方法
  };
}

实战应用示例

示例1:表单编辑历史

function UserForm() {
  const { state: formData, setState, undo, redo, canUndo, canRedo } = useHistoryState(
    { name: '', email: '', age: '' },
    { limit: 20 }
  );

  const handleChange = (field: keyof typeof formData, value: string) => {
    setState({ ...formData, [field]: value });
  };

  return (
    <div>
      <input
        value={formData.name}
        onChange={(e) => handleChange('name', e.target.value)}
        placeholder="姓名"
      />
      <input
        value={formData.email}
        onChange={(e) => handleChange('email', e.target.value)}
        placeholder="邮箱"
      />
      <input
        value={formData.age}
        onChange={(e) => handleChange('age', e.target.value)}
        placeholder="年龄"
      />
      
      <div>
        <button onClick={undo} disabled={!canUndo}>撤销</button>
        <button onClick={redo} disabled={!canRedo}>重做</button>
      </div>
    </div>
  );
}

示例2:图形绘制应用

function DrawingApp() {
  const { state: drawing, setState, undo, redo } = useEnhancedHistoryState<Shape[]>(
    [],
    { 
      limit: 100,
      onStateChange: (shapes, action) => {
        console.log(`操作类型: ${action}, 图形数量: ${shapes.length}`);
      }
    }
  );

  const addShape = (shape: Shape) => {
    setState([...drawing, shape]);
  };

  const clearAll = () => {
    setState([]);
  };

  return (
    <div>
      <Canvas shapes={drawing} onAddShape={addShape} />
      <Toolbar onUndo={undo} onRedo={redo} onClear={clearAll} />
      <HistoryPanel history={drawing} />
    </div>
  );
}

性能优化策略

1. 状态序列化优化

// 使用结构化克隆进行深度比较
const optimizedEqualityFn = (a: any, b: any) => {
  return JSON.stringify(a) === JSON.stringify(b);
};

// 或者使用更高效的比较库
import { isEqual } from 'lodash-es';

2. 内存管理

function useMemoryOptimizedHistory<T>(initialState: T, limit: number = 30) {
  // 使用WeakRef避免内存泄漏
  const stateRef = useRef(new WeakMap<object, number>());
  
  // 实现状态压缩和垃圾回收
  const compressHistory = useCallback(() => {
    // 定期清理不再使用的状态引用
  }, []);
  
  // 使用requestIdleCallback进行后台清理
  useEffect(() => {
    const id = requestIdleCallback(compressHistory);
    return () => cancelIdleCallback(id);
  }, [compressHistory]);
}

3. 批量操作支持

interface BatchOperation<T> {
  operations: T[];
  timestamp: number;
  description?: string;
}

function useBatchHistory<T>() {
  const [batchMode, setBatchMode] = useState(false);
  const [batchOperations, setBatchOperations] = useState<T[]>([]);
  
  const startBatch = () => setBatchMode(true);
  const endBatch = () => {
    setBatchMode(false);
    if (batchOperations.length > 0) {
      // 将批量操作作为单个历史记录项
      setState(batchOperations[batchOperations.length - 1]);
    }
    setBatchOperations([]);
  };
  
  const addToBatch = (operation: T) => {
    if (batchMode) {
      setBatchOperations(prev => [...prev, operation]);
    } else {
      setState(operation);
    }
  };
  
  return { startBatch, endBatch, addToBatch, batchMode };
}

测试策略

单元测试示例

describe('useHistoryState', () => {
  test('应该正确记录状态历史', () => {
    const { result } = renderHook(() => useHistoryState(0));
    
    act(() => {
      result.current.setState(1);
      result.current.setState(2);
    });
    
    expect(result.current.state).toBe(2);
    expect(result.current.canUndo).toBe(true);
    
    act(() => {
      result.current.undo();
    });
    
    expect(result.current.state).toBe(1);
    expect(result.current.canRedo).toBe(true);
  });

  test('应该遵守历史记录限制', () => {
    const limit = 3;
    const { result } = renderHook(() => useHistoryState(0, limit));
    
    // 添加超过限制的操作
    for (let i = 1; i <= limit + 2; i++) {
      act(() => {
        result.current.setState(i);
      });
    }
    
    // 最早的历史记录应该被移除
    expect(result.current.history.past.length).toBe(limit);
  });
});

最佳实践总结

场景推荐配置注意事项
表单编辑limit: 20-50避免记录每个按键操作,使用防抖
图形绘制limit: 100-200考虑状态序列化性能
文本编辑limit: 50-100支持批量操作模式
数据配置limit: 30-50提供操作描述信息

关键收获

  1. 状态隔离: 历史管理应与业务逻辑分离
  2. 性能优先: 合理设置历史记录限制,避免内存溢出
  3. 用户体验: 提供清晰的操作反馈和历史可视化
  4. 测试覆盖: 确保撤销重做功能的可靠性

下一步行动

  • 实现历史记录的可视化面板
  • 添加操作描述和时间戳
  • 支持历史记录的导入导出
  • 集成到现有的React Stately组件中

通过本文的实施方案,你可以为任何React应用添加专业的撤销重做功能,显著提升用户体验和操作安全性。

【免费下载链接】react-spectrum 一系列帮助您构建适应性强、可访问性好、健壮性高的用户体验的库和工具。 【免费下载链接】react-spectrum 项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值