React管理状态的历史

1,281 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

相信大家都已经对React的状态管理相当熟悉了,但是不知道各位有没有遇到过需要对状态进行回退或前进的情况,下面小编就来简单聊聊怎么管理状态的历史。

有一种比较常见的需求:
就是页面上有一项数据是需要打开弹框选择的,这时候就涉及一个数据同步问题,页面的是原始数据,弹框的是编辑数据。

image.png 1、打开弹框时会把页面原始数据同步到弹框内——>>当用户点击确定时,需要把新数据同步到原始数据;
2、当用户点击取消时——>>需要把编辑数据恢复成原始数据。 所以我们要写一个forward方法和一个backward方法,以下是伪代码。

const [initialData, setInitialData] = useState('naruto');
const [currentData, setCurrentData] = useState(initialData);

const forward = () => {
	setInitialData(currentData);
}

const backward = () => {
	setCurrentData(initialData);
}

return (
	<>
		<button onClick={backward}>取消</button>
		<button onClick={forward}>确定</button>
	</>
)

这是比较简单的交互,只有两个时间点的数据互相同步,那如果复杂一点呢? image.png 一个复杂的可编辑表格,还带回退和前进功能,这时候我们就需要把状态的每一次变化节点记录下来,最好是能把这个功能抽象成一个hook,像用useState一样方便。

刚好小编最近深度依赖着ahooks(ahooks刚发布了3.0版本,新增了不少实用的hook),里面提供了一个hook叫useHistoryTravel,看了下API发现完全符合需求。

import { useHistoryTravel } from 'ahooks';
import React from 'react';
 
export default () => {
  const { value, setValue, backLength, forwardLength, back, forward } = useHistoryTravel<string>();
 
  return (
    <div>
      <input value={value || ''} onChange={(e) => setValue(e.target.value)} />
      <button disabled={backLength <= 0} onClick={back} style={{ margin: '0 8px' }}>
        back
      </button>
      <button disabled={forwardLength <= 0} onClick={forward}>
        forward
      </button>
    </div>
  );
};

在初始化的时候就提供了back方法和forward方法可以对状态进行回退或前进,非常方便,那么这个hook是怎么实现的呢?我们来看看它的源码:

const [history, setHistory] = useState<IData<T | undefined>>({
  present: initialValue,
  past: [],
  future: [],
});

它在内部定义了一个叫history的状态,里面包含了一个当前状态,一个过去状态的数组和一个未来状态的数组。由此可见状态的每一次变化都被记录了下来,然后通过backward和forward方法让状态随意穿越。

const _forward = (step: number = 1) => {
   if (future.length === 0) {
     return;
   }
   const { _before, _current, _after } = split(step, future);
   setHistory({
     past: [...past, present, ..._before],
     present: _current,
     future: _after,
   });
 };
 
 const _backward = (step: number = -1) => {
   if (past.length === 0) {
     return;
   }
 
   const { _before, _current, _after } = split(step, past);
   setHistory({
     past: _before,
     present: _current,
     future: [..._after, present, ...future],
   });
 };

浏览了一遍源码后就发现,这个看似复杂的功能原来可以如此简单就能实现。

虽然功能实现了,但是还是感觉缺了点什么,要是能够像PS给历史记录打快照一样,给状态也打个快照那就更完善了。

而事实上,确实需要这样一个功能,当你编辑完需要保存时,其实就是打一个快照功能,当你下一次编辑过后想取消的时候就可以恢复到最近的一个快照状态。

那么我们再来看看useHistoryTravel的API,发现它的reset方法是可以传入一个新状态的,然后把这个新状态覆盖掉初始化时定义的原始状态,然后下一次调用reset方法时,

状态就会就会恢复到最近一次被覆盖的原始状态上。

const reset = (...params: any[]) => {
  const _initial = params.length > 0 ? params[0] : initialValueRef.current;
  initialValueRef.current = _initial;
 
  setHistory({
    present: _initial,
    future: [],
    past: [],
  });
};

一个简单的打快照功能算是能实现了,可是从代码上看到,每次reset,past和future都被清空了,难道保存后就不让回退吗?这明显不符合逻辑,于是小编用了一个简单有效的方法,

const { value: dataSource, setValue: setDataSource } = useHistoryTravel();
const { value: snapshot, setValue: setSnapshot} = useHistoryTravel();

单独再定义一个状态专门管理快照就好啦,哈哈。

当然,如果希望能够实现得更完美,那当然是直接在源码基础上进行扩展,以小编的愚见,可以把状态定义改成以下结构:

interface IValue<T> {
  value?: T;
  id: string;
  timestamp: number;
  snapshot: boolean;
}
 
interface IData<T> {
  current?: T;
  present: IValue<T>;
  past: IValue<T>[];
  future: IValue<T>[];
}

其实就是把状态值存在一个对象里,每当初始化或者更新时都加上唯一的id、时间戳和快照状态等等,这些都是非常有用的信息,有了这些信息,使用起来就更加灵活哈哈。