- Published on
TOC Component Bug Report
- Authors

- 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.
根本原因:
onToggle事件的频繁触发:- 双击操作会触发多次
onToggle事件,导致isOpen状态被频繁更新,组件状态不稳定。
- 双击操作会触发多次
- 原生行为与 React 状态的冲突:
<details>元素的open属性与 React 的isOpen状态未完全同步,导致状态更新不一致。
- 防抖逻辑不足:
- 原有的防抖逻辑未能有效阻止快速双击或频繁点击导致的状态更新。
Root Causes:
- Frequent triggering of
onToggleevents:- Double-click operations trigger multiple
onToggleevents, causing theisOpenstate to be updated frequently and making the component state unstable.
- Double-click operations trigger multiple
- Conflict between native behavior and React state:
- The
openattribute of the<details>element is not fully synchronized with React'sisOpenstate, leading to inconsistent state updates.
- The
- Insufficient debounce logic:
- The original debounce logic failed to effectively prevent state updates caused by rapid double-clicks or frequent clicks.
- Frequent triggering of
防抖的处理方法 | Debouncing Solutions
1. 阻止原生行为 | Preventing Native Behavior
问题:
<details>元素的onToggle事件会触发原生行为,导致状态更新与 React 状态不同步。Problem: The
onToggleevent 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()inhandleToggleto prevent the default behavior of the<details>element. - Let React fully control state updates, avoiding interference from native behavior.
- Use
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
setTimeoutto implement debouncing and delay state updates. - Use
isTogglingRefto 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.
- Use
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
openattribute of the<details>element may not be synchronized with React'sisOpenstate.解决方案:
- 使用
useEffect手动同步<details>的open属性与 React 的isOpen状态,确保两者保持一致。
- 使用
Solution:
- Use
useEffectto manually synchronize theopenattribute of<details>with React'sisOpenstate, ensuring both remain consistent.
- Use
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
useMemoto cache the filtered table of contents list (filteredToc), avoiding additional renders caused by state updates.
- Use
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 multipleonToggleevents, causing theisOpenstate to be updated frequently.进一步分析发现,
<details>元素的open属性与 React 的isOpen状态不同步,导致循环更新。Further analysis revealed that the
openattribute of the<details>element is not synchronized with React'sisOpenstate, 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.
增强防抖逻辑:使用
setTimeout和isTogglingRef标记,确保防抖逻辑能够有效阻止频繁的状态更新。Enhanced debounce logic: Use
setTimeoutandisTogglingRefflag to ensure debounce logic effectively prevents frequent state updates.手动同步状态:使用
useEffect确保<details>的open属性与 React 的isOpen状态完全同步。Manual state synchronization: Use
useEffectto ensure theopenattribute of<details>is fully synchronized with React'sisOpenstate.减少不必要的渲染:使用
useMemo缓存过滤后的目录列表,优化性能。Reduce unnecessary renders: Use
useMemoto cache the filtered table of contents list, optimizing performance.
3. 代码实现 | Code Implementation
在
handleToggle中阻止原生行为,并实现防抖逻辑。Prevent native behavior in
handleToggleand implement debounce logic.使用
useEffect手动同步<details>的open属性。Use
useEffectto manually synchronize theopenattribute of<details>.使用
useMemo缓存过滤后的目录列表。Use
useMemoto 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
openattribute of<details>is fully synchronized with React'sisOpenstate.性能测试:确保组件在频繁交互时不会出现性能问题,页面响应流畅。
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:
阻止原生行为:确保状态更新由 React 完全控制,避免原生行为干扰。
Prevent native behavior: Ensure state updates are fully controlled by React, avoiding interference from native behavior.
增强防抖逻辑:使用
setTimeout和isTogglingRef标记,有效阻止频繁的状态更新。Enhanced debounce logic: Use
setTimeoutandisTogglingRefflag to effectively prevent frequent state updates.手动同步状态:确保
<details>的open属性与 React 的isOpen状态完全同步。Manual state synchronization: Ensure the
openattribute of<details>is fully synchronized with React'sisOpenstate.减少不必要的渲染:优化性能,避免因状态更新导致的额外渲染。
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.