发布于 2026-01-06 3 阅读
0

React Hook 实现撤销/重做

React Hook 实现撤销/重做

如果你想开发像FormBlob这样的无代码工具,那么撤销和重做功能是必不可少的。为什么呢?想象一下,你正在使用一款图像编辑软件,并对画布进行了多次修改。过了一段时间后,你意识到之前的版本比现在的版本看起来好得多。这时,你会撤销操作,直到达到你满意的程度。

如果软件没有撤销/重做功能,你很可能会破口大骂,然后永远放弃这款软件。

那么,我们如何实现撤销/重做功能,防止用户放弃我们的应用呢?

先决条件

如果您还不熟悉 React Hooks,我建议您先阅读一下相关资料(链接在此)。其中最基础的 Hook 之一是 React 内置的 useState Hook。它允许您将组件状态存储在一个变量中,并根据需要进行管理。在本教程中,我们将编写一个扩展 useState Hook 的 Hook,以实现撤销/重做功能。

代码

我们先来看代码,然后我在下面进行解释。

import { useMemo, useState } from "react";
// If you're only working with primitives, this is not required
import isEqual from "lodash/isEqual";

export default function useUndoableState(init) {
  const [states, setStates] = useState([init]); // Used to store history of all states
  const [index, setIndex] = useState(0); // Index of current state within `states`

  const state = useMemo(() => states[index], [states, index]); // Current state

  const setState = (value) => {
    // Use lodash isEqual to check for deep equality
    // If state has not changed, return to avoid triggering a re-render
    if (isEqual(state, value)) {
      return;
    }
    const copy = states.slice(0, index + 1); // This removes all future (redo) states after current index
    copy.push(value);
    setStates(copy);
    setIndex(copy.length - 1);
  };

  // Clear all state history
  const resetState = (init) => {
    setIndex(0);
    setStates([init]);
  };

  // Allows you to go back (undo) N steps
  const goBack = (steps = 1) => {
    setIndex(Math.max(0, Number(index) - (Number(steps) || 1)));
  };

  // Allows you to go forward (redo) N steps
  const goForward = (steps = 1) => {
    setIndex(Math.min(states.length - 1, Number(index) + (Number(steps) || 1)));
  };

  return {
    state,
    setState,
    resetState,
    index,
    lastIndex: states.length - 1,
    goBack,
    goForward,
  };
}
Enter fullscreen mode Exit fullscreen mode

概念

与 useState 类似,useUndoableState 也只接受一个参数,即初始值。在底层,该钩子函数使用两个主要变量来确定状态—— index(数字)和states(数组)。(数字)states存储状态的历史值,而(数组)则index通过指示当前在数组中的位置来确定当前状态。

goBack您可以使用钩子发出的 ` and`函数来遍历历史状态goForward。但是,如果您调用 ` setStateand`函数时当前状态index不在数组末尾states,则该状态之后的所有状态都index将被清除,index您将返回到数组末尾states。换句话说,一旦调用 `and` 函数setState,您就无法再进行重做操作。

下表试图对钩子返回的对象提供更详细的解释:

支柱 类型 用法 描述
状态 any 当前状态,已通过传递的参数初始化。
设置状态 func setState(value) 将状态设置为value. 当前值之后的所有值都index将被清除。
重置状态 func resetState(value) 删除历史状态并重置为值
指数 number states数组中的当前索引
lastIndex number 数组中的最后一个索引states。可用于确定是否可以goForwardcanGoForward = index < lastIndex
返回 func goBack(2) 回溯已走过的步数
前进 func goForward(3) 向前推进已走过的步数

用法

import React from "react";
import useUndoableState from "path/to/hook";

const init = { text: "The quick brown fox jumps over the lazy dog" };

export default function Document() {
  const {
    state: doc,
    setState: setDoc,
    resetState: resetDoc,
    index: docStateIndex,
    lastIndex: docStateLastIndex,
    goBack: undoDoc,
    goForward: redoDoc
  } = useUndoableState(init);

  const canUndo = docStateIndex > 0;
  const canRedo = docStateIndex < docStateLastIndex;

  return (
    <div style={{ display: "block" }}>
      <textarea
        style={{ margin: "16px" }}
        onChange={(event) => setDoc({ text: event.target.value })}
        rows="5"
        value={doc.text}
      />
      <div>
        <button
          onClick={() => undoDoc()}
          disabled={!canUndo}
          style={{ marginRight: "8px" }}
        >
          Undo
        </button>
        <button
          onClick={() => redoDoc()}
          disabled={!canRedo}
          style={{ marginRight: "8px" }}
        >
          Redo
        </button>
        <button onClick={() => resetDoc(init)}>Reset</button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

总结发言

FormBlob具备撤销/重做功能,是少数几款无需编写代码即可创建表单的工具之一,让您可以灵活地编辑表单,而无需担心丢失之前的状态。作为一款无需代码的工具,FormBlob 让任何人都能在 2 分钟内创建并发布精美的表单和调查问卷。立即免费试用!

文章来源:https://dev.to/jeremyling/react-hook-to-allow-undoredo-4poj