You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native不使用Redux实现撤销/重做、图片旋转及缩放方案问询

No-Redux Undo/Redo Demo with Image Manipulation & Text Drag

Hey there! I totally get wanting to implement undo/redo without adding extra libraries like Redux—plain state management can be clean and efficient for this use case. Below is a complete React demo that covers all your requirements: image click tracking, text drag-and-drop, image rotation/scale, and full undo/redo functionality using the command pattern (perfect for capturing reversible actions).


Full Working Demo Code

import { useState, useRef } from 'react';

// Base command structure - every action implements execute and undo logic
class Command {
  constructor(execute, undo) {
    this.execute = execute;
    this.undo = undo;
  }
}

// Create a rotation command for images
const createRotateCommand = (currentState, degrees) => {
  const prevRotation = currentState.image.rotation;
  const newRotation = prevRotation + degrees;
  
  return new Command(
    () => ({ ...currentState, image: { ...currentState.image, rotation: newRotation } }),
    () => ({ ...currentState, image: { ...currentState.image, rotation: prevRotation } })
  );
};

// Create a scaling command for images
const createScaleCommand = (currentState, factor) => {
  const prevScale = currentState.image.scale;
  const newScale = Math.max(0.5, Math.min(3, prevScale * factor)); // Clamp to prevent extreme values
  
  return new Command(
    () => ({ ...currentState, image: { ...currentState.image, scale: newScale } }),
    () => ({ ...currentState, image: { ...currentState.image, scale: prevScale } })
  );
};

// Create a text drag command
const createDragTextCommand = (currentState, newPosition) => {
  const prevPosition = currentState.text.position;
  
  return new Command(
    () => ({ ...currentState, text: { ...currentState.text, position: newPosition } }),
    () => ({ ...currentState, text: { ...currentState.text, position: prevPosition } })
  );
};

// Create an image click tracking command
const createImageClickCommand = (currentState) => {
  const prevClickCount = currentState.image.clickCount;
  
  return new Command(
    () => ({ ...currentState, image: { ...currentState.image, clickCount: prevClickCount + 1 } }),
    () => ({ ...currentState, image: { ...currentState.image, clickCount: prevClickCount } })
  );
};

function App() {
  // Initial app state
  const initialState = {
    image: {
      src: 'https://picsum.photos/400/300', // Placeholder image
      rotation: 0,
      scale: 1,
      clickCount: 0
    },
    text: {
      content: 'Drag me around!',
      position: { x: 50, y: 200 }
    }
  };

  // Core state for undo/redo logic
  const [currentState, setCurrentState] = useState(initialState);
  const [historyStack, setHistoryStack] = useState([]);
  const [redoStack, setRedoStack] = useState([]);
  
  // Refs for text drag tracking
  const textRef = useRef(null);
  const isDragging = useRef(false);
  const dragStartPos = useRef({ x: 0, y: 0 });

  // Execute a new action and update history
  const executeCommand = (command) => {
    setHistoryStack(prev => [...prev, currentState]);
    setRedoStack([]); // Clear redo stack when new actions are taken
    setCurrentState(command.execute());
  };

  // Undo the last action
  const handleUndo = () => {
    if (historyStack.length === 0) return;
    const prevState = historyStack[historyStack.length - 1];
    setRedoStack(prev => [...prev, currentState]);
    setHistoryStack(prev => prev.slice(0, -1));
    setCurrentState(prevState);
  };

  // Redo the last undone action
  const handleRedo = () => {
    if (redoStack.length === 0) return;
    const nextState = redoStack[redoStack.length - 1];
    setHistoryStack(prev => [...prev, currentState]);
    setRedoStack(prev => prev.slice(0, -1));
    setCurrentState(nextState);
  };

  // Text drag event handlers
  const handleTextMouseDown = (e) => {
    isDragging.current = true;
    dragStartPos.current = {
      x: e.clientX - currentState.text.position.x,
      y: e.clientY - currentState.text.position.y
    };
  };

  const handleTextMouseMove = (e) => {
    if (!isDragging.current) return;
    const newPosition = {
      x: e.clientX - dragStartPos.current.x,
      y: e.clientY - dragStartPos.current.y
    };
    // Update temp state during drag (finalize command on mouse up)
    setCurrentState(prev => ({ ...prev, text: { ...prev.text, position: newPosition } }));
  };

  const handleTextMouseUp = () => {
    if (!isDragging.current) return;
    // Create a permanent drag command for undo/redo
    const dragCommand = createDragTextCommand(historyStack[historyStack.length - 1] || initialState, currentState.text.position);
    executeCommand(dragCommand);
    isDragging.current = false;
  };

  return (
    <div style={{ padding: '20px', position: 'relative', minHeight: '600px' }}>
      <div style={{ marginBottom: '20px' }}>
        <button onClick={handleUndo} disabled={historyStack.length === 0} style={{ marginRight: '10px' }}>
          Undo
        </button>
        <button onClick={handleRedo} disabled={redoStack.length === 0}>
          Redo
        </button>
      </div>

      <div style={{ marginBottom: '20px' }}>
        <button onClick={() => executeCommand(createRotateCommand(currentState, 90))} style={{ marginRight: '10px' }}>
          Rotate +90°
        </button>
        <button onClick={() => executeCommand(createRotateCommand(currentState, -90))} style={{ marginRight: '10px' }}>
          Rotate -90°
        </button>
        <button onClick={() => executeCommand(createScaleCommand(currentState, 1.2))} style={{ marginRight: '10px' }}>
          Zoom In
        </button>
        <button onClick={() => executeCommand(createScaleCommand(currentState, 0.8))}>
          Zoom Out
        </button>
      </div>

      <div style={{ marginBottom: '10px' }}>
        Image clicked {currentState.image.clickCount} times
      </div>

      {/* Interactive Image */}
      <img
        src={currentState.image.src}
        alt="Demo"
        style={{
          transform: `rotate(${currentState.image.rotation}deg) scale(${currentState.image.scale})`,
          transition: 'transform 0.3s ease',
          cursor: 'pointer',
          position: 'absolute',
          top: '150px',
          left: '50px'
        }}
        onClick={() => executeCommand(createImageClickCommand(currentState))}
      />

      {/* Draggable Text */}
      <div
        ref={textRef}
        style={{
          position: 'absolute',
          left: `${currentState.text.position.x}px`,
          top: `${currentState.text.position.y}px`,
          cursor: 'move',
          padding: '8px 12px',
          backgroundColor: '#f0f0f0',
          border: '1px solid #ccc'
        }}
        onMouseDown={handleTextMouseDown}
        onMouseMove={handleTextMouseMove}
        onMouseUp={handleTextMouseUp}
        onMouseLeave={handleTextMouseUp}
      >
        {currentState.text.content}
      </div>
    </div>
  );
}

export default App;

Key Concepts Explained

  • Command Pattern: Every reversible action (rotate, scale, drag, click) is wrapped in a Command object with execute() (applies the action) and undo() (reverts the action) methods. This keeps state changes predictable and easy to reverse.
  • Undo/Redo State Flow: We use three core state variables:
    • currentState: The active state of the app
    • historyStack: Stores past states for undo operations
    • redoStack: Stores undone states for redo operations
    • New actions clear the redo stack, while undo/redo moves states between the two stacks to maintain history.
  • Image Controls: Rotation and scaling use CSS transforms, with state values tracked to ensure full reversibility. Click counts are stored in state and rolled back on undo.
  • Text Drag: Real-time position updates during drag, with a single command created on mouse release to make the entire drag action undoable in one step.

This demo is self-contained—you can drop it into a React project and run it immediately. All actions are fully undoable/redoable, and there's no Redux or external state libraries required. Let me know if you need help adapting this to your specific tech stack!

内容的提问来源于stack exchange,提问作者Lavaraju

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:13:07