react-undo-redo-history
test12026-10-2615 次阅读
为什么需要历史栈
富文本编辑、表单设计器、画布拖拽这类场景,用户会反复改动状态。如果只存当前值,一旦改错就回不去了。把每一次变更记进一个历史栈,撤销就是弹栈、重做就是把弹出的值再压回去,逻辑非常干净。
设计状态结构
把状态拆成三段:过去、现在、未来。初始时过去和未来都是空的,只有现在。
interface HistoryState<T> {
past: T[];
present: T;
future: T[];
}
type Action<T> =
| { type: "SET"; payload: T }
| { type: "UNDO" }
| { type: "REDO" };
实现 reducer
SET 把当前 present 推入 past、清空 future;UNDO 从 past 取出最后一项作为新的 present,并把原 present 推入 future;REDO 反向操作。
function historyReducer<T>(
state: HistoryState<T>,
action: Action<T>
): HistoryState<T> {
const { past, present, future } = state;
switch (action.type) {
case "SET": {
if (action.payload === present) return state;
return { past: [...past, present], present: action.payload, future: [] };
}
case "UNDO": {
if (past.length === 0) return state;
const previous = past[past.length - 1];
return {
past: past.slice(0, -1),
present: previous,
future: [present, ...future],
};
}
case "REDO": {
if (future.length === 0) return state;
const next = future[0];
return {
past: [...past, present],
present: next,
future: future.slice(1),
};
}
}
}
封装成 useHistory Hook
把 reducer 和 dispatch 包进一个 Hook,对外只暴露 set、undo、redo,以及 canUndo/canRedo 两个布尔,用来控制按钮可用态。
import { useReducer, useCallback } from "react";
export function useHistory<T>(initial: T) {
const [state, dispatch] = useReducer(historyReducer, {
past: [],
present: initial,
future: [],
});
const set = useCallback(
(value: T) => dispatch({ type: "SET", payload: value }),
[]
);
const undo = useCallback(() => dispatch({ type: "UNDO" }), []);
const redo = useCallback(() => dispatch({ type: "REDO" }), []);
return {
state: state.present,
set,
undo,
redo,
canUndo: state.past.length > 0,
canRedo: state.future.length > 0,
};
}
在组件里使用
编辑器把输入框的值接到 state,按钮按 canUndo/canRedo 决定能不能点。整个撤销重做链路不需要组件自己维护数组。
function Editor() {
const { state, set, undo, redo, canUndo, canRedo } = useHistory("");
return (
<div>
<textarea value={state} onChange={(e) => set(e.target.value)} />
<button onClick={undo} disabled={!canUndo}>撤销</button>
<button onClick={redo} disabled={!canRedo}>重做</button>
</div>
);
}
工程注意点
- past 和 future 会随操作无限增长,长会话要设一个最大长度,超出就丢弃最旧的一段。
- SET 里做了相等判断,连续两次相同输入不会污染历史。
- 若状态是对象,记得每次 SET 都传新引用,否则相等判断会失效。
- 需要把整段历史存盘时,直接把 HistoryState 序列化进 localStorage 即可。
T
test1
文章作者
为什么需要历史栈 富文本编辑、表单设计器、画布拖拽这类场景,用户会反复改动状态。如果只存当前值,一旦改错就回不去了。把...
- 分类
- 技术
- 发布时间
- 2026-10-26
- 字数
- 约 2548 字
- 阅读
- 15 次
