Nebula 的每次切换账号或目录都会改 current / path 这两个 state。给它加前进 / 后退,直觉做法是拿一个 useState 数组当历史栈,每次 current/path 变化就 push。这里有个死循环等着你:后退本身也会改 current/path,如果后退动作也被当成"新的一次导航"记录进去,历史栈会不断把后退动作又摞成新的一条前进记录,后退键完全不可用。

用 ref 存历史栈,而不是 state

历史栈的写入不需要触发渲染(渲染由 current/path 自己的 state 负责),所以用 useRef 存,配一个手动的重渲染信号:

const navHistory = useRef<{ account: string | null; path: string }[]>([]);
const navIdx = useRef(-1);
const navBack = useRef(false);      // 本次变化是"导航动作"引起的,别再入栈
const [navVer, setNavVer] = useState(0);  // 只用来强制刷新 canBack/canForward

用一个标志位打断循环

goHistory 在跳转前先把 navBack.current 置位;监听 current/path 的 effect 一看到这个标志就直接消费掉、不入栈:

useEffect(() => {
  if (navBack.current) { navBack.current = false; return; }  // 这次是后退/前进本身,跳过
  const cut = navHistory.current.slice(0, navIdx.current + 1);
  const last = cut[cut.length - 1];
  if (last && last.account === current && last.path === path) return; // 去重
  cut.push({ account: current, path });
  navHistory.current = cut;
  navIdx.current = cut.length - 1;
  setNavVer((v) => v + 1);
}, [current, path]);

const goHistory = (delta: number) => {
  const target = navIdx.current + delta;
  if (target < 0 || target >= navHistory.current.length) return;
  navIdx.current = target;
  navBack.current = true;                 // 打断上面的入栈逻辑
  const e = navHistory.current[target];
  if (e.account !== current) setCurrent(e.account);
  setPath(e.path);
  setNavVer((v) => v + 1);
};

跳到某个历史节点之后再手动导航到别处,会先 slice(0, navIdx + 1) 截断掉"未来"的分支——和浏览器历史的行为一致:后退几步再点别的链接,原来的前进历史就没了。

入口:工具栏按钮 + 快捷键

Alt/⌘ + ←/→ 绑定到 goHistory(-1) / goHistory(1),工具栏也给了一对图标按钮,canBack/canForwardnavIdx 相对栈的位置算出,用来控制按钮的禁用态。

小结

「记一串状态、状态变化时自动入栈」这个模式一旦遇到"程序自己触发的状态变化不该被记录"的场景,就得有一个标志位来分辨"这次变化是用户的新动作"还是"这次变化是回放历史的结果"——historyStack 用 ref 存也是同一个道理:它是导航层的账本,不该被当成渲染要用的数据。