You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在React中使用firstElementChild等直接操作DOM是否是不良实践?

关于React手风琴组件直接操作DOM的合理性及优化建议

首先明确:React里直接操作DOM并非绝对的不良实践,在动画、精确DOM控制这类场景下,直接操作DOM反而比强行用状态驱动更高效简洁——你的思路完全没问题,而且比那些依赖第三方库或用setTimeout的方案更优雅。

你的实现优点

  • 用useRef存储DOM引用和展开状态,避免了不必要的组件重渲染,性能表现好
  • 直接修改DOM样式和ARIA属性,逻辑直观,没有冗余代码
  • 加入了按钮临时禁用的逻辑,防止重复点击触发多次动画,细节考虑到位

潜在的待优化点

  1. 列表key的问题
    你用Math.random()生成列表项的key,这会导致每次组件重渲染时都生成新key,React会销毁并重建所有列表项组件,不仅会丢失组件内部状态,还会严重影响性能,甚至可能导致tabsRef的引用出错。建议替换为item的唯一标识(比如item.id),如果没有唯一标识,用索引i(前提是items不会重新排序)。

  2. Ref引用的健壮性
    在ContentInner的ref回调中存储element?.parentElement,React的ref回调会在组件挂载、卸载、更新时触发,若父组件props变化导致items重新渲染,可能出现ref引用不准确或null的情况。可以在ref回调中处理卸载场景:

    ref={(element) => {
      if (element) {
        tabsRef.current[i] = { index: i, element: element.parentElement as HTMLElement };
      } else {
        // 组件卸载时移除对应ref
        tabsRef.current.splice(i, 1);
      }
    }}
    
  3. ARIA属性的一致性
    初始渲染时AccordionTriggerWrapper的aria-expanded硬编码为"false",如果存在默认展开的项,DOM属性会和实际状态不一致。建议让ARIA属性由状态驱动,比如用useState存储展开索引(而非纯useRef),这样初始状态可以通过props控制,渲染时直接传递:

    const [expandedIndexes, setExpandedIndexes] = useState<number[]>([]);
    // 渲染时
    <AccordionTriggerWrapper
      aria-expanded={expandedIndexes.includes(i).toString()}
      // ...其他属性
    >
    

    这样虽然会触发组件重渲染,但手风琴组件的项数通常不多,性能影响可以忽略,同时保证了声明式的状态一致性。

  4. DOM结构依赖风险
    代码中用firstElementChild、closest("[aria-expanded]")依赖当前DOM结构,若后续修改组件的DOM层级(比如调整Content的子元素、修改触发器的包裹结构),这些方法会直接失效。建议在渲染时直接存储触发器的ref:

    const triggerRefs = useRef<HTMLElement[]>([]);
    // 渲染触发器时
    <AccordionTriggerWrapper
      ref={(el) => triggerRefs.current[i] = el}
      // ...其他属性
    >
    

    之后在alterTabs中直接使用triggerRefs.current[tab.index]来设置ARIA属性,无需依赖DOM结构查找。

  5. 样式逻辑耦合
    直接用style.setProperty修改height,把样式逻辑写在了JS中,不符合关注点分离。可以改用CSS类+CSS变量的方式:

    .Content {
      height: 0;
      overflow: hidden;
      transition: height 0.3s ease;
    }
    .Content.is-expanded {
      height: var(--content-height);
    }
    

    然后在JS中只负责计算高度并切换类:

    tab.element.style.setProperty("--content-height", newIndexes.includes(tab.index) ? `${tab.element.firstElementChild.clientHeight}px` : "0px");
    tab.element.classList.toggle("is-expanded", newIndexes.includes(tab.index));
    

    这样样式逻辑集中在CSS中,JS只处理动态计算部分,代码更易维护。

总结

你的实现本身是可行且高效的,只要解决上述几个潜在问题,代码会更健壮、可维护。相比那些依赖第三方库或用setTimeout的方案,你的思路更简洁直接,完全符合React在特定场景下允许直接操作DOM的原则。

内容的提问来源于stack exchange,提问作者MakingStuffs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 07:20:54