Kto-Blog
Published on

TOC Component Bug Report

Authors
  • avatar
    Name
    Kto

Bug Summary

这是我在为自己博客添加目录组件的过程中碰到的一个BUG,刚好记录一下。

This is a bug I encountered while adding a table of contents component to my blog. I decided to document it for reference.

问题描述 | Problem Description

  • 现象:在测试过程中,发现双击目录的展开/收缩按钮后,组件会进入无限循环的展开和折叠状态,导致页面卡顿,用户体验较差。

  • Symptom: During testing, I discovered that double-clicking the expand/collapse button of the table of contents caused the component to enter an infinite loop of expanding and collapsing, resulting in page lag and poor user experience.

  • 根本原因

    1. onToggle 事件的频繁触发
      • 双击操作会触发多次 onToggle 事件,导致 isOpen 状态被频繁更新,组件状态不稳定。
    2. 原生行为与 React 状态的冲突
      • <details> 元素的 open 属性与 React 的 isOpen 状态未完全同步,导致状态更新不一致。
    3. 防抖逻辑不足
      • 原有的防抖逻辑未能有效阻止快速双击或频繁点击导致的状态更新。
  • Root Causes:

    1. Frequent triggering of onToggle events:
      • Double-click operations trigger multiple onToggle events, causing the isOpen state to be updated frequently and making the component state unstable.
    2. Conflict between native behavior and React state:
      • The open attribute of the <details> element is not fully synchronized with React's isOpen state, leading to inconsistent state updates.
    3. Insufficient debounce logic:
      • The original debounce logic failed to effectively prevent state updates caused by rapid double-clicks or frequent clicks.

防抖的处理方法 | Debouncing Solutions

1. 阻止原生行为 | Preventing Native Behavior

  • 问题<details> 元素的 onToggle 事件会触发原生行为,导致状态更新与 React 状态不同步。

  • Problem: The onToggle event of the <details> element triggers native behavior, causing state updates to be out of sync with React state.

  • 解决方案

    • handleToggle 中使用 e.preventDefault(),阻止 <details> 元素的默认行为。
    • 完全由 React 控制状态更新,避免原生行为干扰。
  • Solution:

    • Use e.preventDefault() in handleToggle to prevent the default behavior of the <details> element.
    • Let React fully control state updates, avoiding interference from native behavior.
const handleToggle = (e: React.SyntheticEvent<HTMLDetailsElement>) => {
  e.preventDefault() // 阻止原生行为
  // 其他逻辑
}

2. 增强防抖逻辑 | Enhanced Debounce Logic

  • 问题:快速双击或频繁点击会触发多次状态更新,导致组件进入无限循环。

  • Problem: Rapid double-clicks or frequent clicks trigger multiple state updates, causing the component to enter an infinite loop.

  • 解决方案

    • 使用 setTimeout 实现防抖,延迟状态更新。
    • 使用 isTogglingRef 标记是否正在切换状态,避免在防抖时间内重复触发状态更新。
    • 在状态更新前清除之前的定时器,确保只有最后一次更新生效。
  • Solution:

    • Use setTimeout to implement debouncing and delay state updates.
    • Use isTogglingRef to mark whether the state is being toggled, avoiding repeated state updates within the debounce time.
    • Clear the previous timer before state updates, ensuring only the last update takes effect.
const timerRef = useRef<NodeJS.Timeout | null>(null) // 用于防抖定时器
const isTogglingRef = useRef<boolean>(false) // 用于标记是否正在切换状态

const handleToggle = (e: React.SyntheticEvent<HTMLDetailsElement>) => {
  e.preventDefault() // 阻止原生行为

  if (isTogglingRef.current) return // 如果正在切换状态,则直接返回
  isTogglingRef.current = true // 标记为正在切换状态

  if (timerRef.current) {
    clearTimeout(timerRef.current)
  }

  timerRef.current = setTimeout(() => {
    setIsOpen((prev) => {
      const newState = !prev
      console.log('Toggling isOpen:', newState) // 调试日志
      return newState
    })
    isTogglingRef.current = false // 重置标记
  }, 100) // 防抖时间设置为 100ms
}

3. 手动同步状态 | Manual State Synchronization

  • 问题<details> 元素的 open 属性与 React 的 isOpen 状态可能不同步。

  • Problem: The open attribute of the <details> element may not be synchronized with React's isOpen state.

  • 解决方案

    • 使用 useEffect 手动同步 <details>open 属性与 React 的 isOpen 状态,确保两者保持一致。
  • Solution:

    • Use useEffect to manually synchronize the open attribute of <details> with React's isOpen state, ensuring both remain consistent.
useEffect(() => {
  if (detailsRef.current) {
    detailsRef.current.open = isOpen
  }
}, [isOpen])

4. 减少不必要的渲染 | Reducing Unnecessary Renders

  • 问题:状态更新可能导致不必要的渲染,影响性能。

  • Problem: State updates may cause unnecessary renders, affecting performance.

  • 解决方案

    • 使用 useMemo 缓存过滤后的目录列表 (filteredToc),避免因状态更新导致的额外渲染。
  • Solution:

    • Use useMemo to cache the filtered table of contents list (filteredToc), avoiding additional renders caused by state updates.
const filteredToc = useMemo(() => {
  return toc.filter(
    (heading) =>
      heading.depth >= fromHeading &&
      heading.depth <= toHeading &&
      (!excludeRegex || !excludeRegex.test(heading.value))
  )
}, [toc, fromHeading, toHeading, excludeRegex])

详细处理思路 | Detailed Problem-Solving Approach

1. 问题定位 | Problem Identification

  • 通过调试日志 (console.log) 发现,双击会触发多次 onToggle 事件,导致 isOpen 状态被频繁更新。

  • Through debug logs (console.log), I discovered that double-clicking triggers multiple onToggle events, causing the isOpen state to be updated frequently.

  • 进一步分析发现,<details> 元素的 open 属性与 React 的 isOpen 状态不同步,导致循环更新。

  • Further analysis revealed that the open attribute of the <details> element is not synchronized with React's isOpen state, leading to cyclic updates.

  • 测试过程中,使用快速双击和频繁点击操作复现了问题,确认了防抖逻辑的不足。

  • During testing, I reproduced the issue using rapid double-clicks and frequent click operations, confirming the inadequacy of the debounce logic.

2. 解决方案设计 | Solution Design

  • 阻止原生行为:确保状态更新由 React 完全控制,避免原生行为干扰。

  • Prevent native behavior: Ensure state updates are fully controlled by React, avoiding interference from native behavior.

  • 增强防抖逻辑:使用 setTimeoutisTogglingRef 标记,确保防抖逻辑能够有效阻止频繁的状态更新。

  • Enhanced debounce logic: Use setTimeout and isTogglingRef flag to ensure debounce logic effectively prevents frequent state updates.

  • 手动同步状态:使用 useEffect 确保 <details>open 属性与 React 的 isOpen 状态完全同步。

  • Manual state synchronization: Use useEffect to ensure the open attribute of <details> is fully synchronized with React's isOpen state.

  • 减少不必要的渲染:使用 useMemo 缓存过滤后的目录列表,优化性能。

  • Reduce unnecessary renders: Use useMemo to cache the filtered table of contents list, optimizing performance.

3. 代码实现 | Code Implementation

  • handleToggle 中阻止原生行为,并实现防抖逻辑。

  • Prevent native behavior in handleToggle and implement debounce logic.

  • 使用 useEffect 手动同步 <details>open 属性。

  • Use useEffect to manually synchronize the open attribute of <details>.

  • 使用 useMemo 缓存过滤后的目录列表。

  • Use useMemo to cache the filtered table of contents list.

4. 测试与验证 | Testing & Validation

  • 快速双击测试:确保双击不会导致无限循环,组件状态稳定。

  • Rapid double-click test: Ensure double-clicking does not cause infinite loops and the component state remains stable.

  • 状态同步测试:检查 <details>open 属性是否与 React 的 isOpen 状态完全同步。

  • State synchronization test: Check whether the open attribute of <details> is fully synchronized with React's isOpen state.

  • 性能测试:确保组件在频繁交互时不会出现性能问题,页面响应流畅。

  • Performance test: Ensure the component does not experience performance issues during frequent interactions and the page responds smoothly.

  • 回归测试:验证修复后的代码是否影响其他功能,确保整体功能正常。

  • Regression test: Verify whether the fixed code affects other functions and ensure overall functionality works correctly.


总结 | Conclusion

通过以上方法,我成功解决了 双击导致无限循环 的问题,并增强了组件的健壮性。以下是关键点:

Through the above methods, I successfully resolved the infinite loop caused by double-clicking issue and enhanced the component's robustness. Here are the key points:

  1. 阻止原生行为:确保状态更新由 React 完全控制,避免原生行为干扰。

  2. Prevent native behavior: Ensure state updates are fully controlled by React, avoiding interference from native behavior.

  3. 增强防抖逻辑:使用 setTimeoutisTogglingRef 标记,有效阻止频繁的状态更新。

  4. Enhanced debounce logic: Use setTimeout and isTogglingRef flag to effectively prevent frequent state updates.

  5. 手动同步状态:确保 <details>open 属性与 React 的 isOpen 状态完全同步。

  6. Manual state synchronization: Ensure the open attribute of <details> is fully synchronized with React's isOpen state.

  7. 减少不必要的渲染:优化性能,避免因状态更新导致的额外渲染。

  8. Reduce unnecessary renders: Optimize performance, avoiding additional renders caused by state updates.

我会在后续的测试中重点关注以下几点:

I will focus on the following points in subsequent testing:

  • 边界测试:测试极端情况下的组件行为,例如快速多次点击、网络延迟等。

  • Boundary testing: Test component behavior under extreme conditions, such as rapid multiple clicks, network latency, etc.

  • 兼容性测试:确保修复后的代码在不同浏览器和设备上表现一致。

  • Compatibility testing: Ensure the fixed code behaves consistently across different browsers and devices.

  • 性能监控:通过性能分析工具监控组件的渲染性能,确保优化效果符合预期。

  • Performance monitoring: Monitor the component's rendering performance through performance analysis tools to ensure optimization results meet expectations.

Comments

Join the discussion and share your thoughts

Sort after loading
Sign in to post comments and replies