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

React+MUI:折叠面板展开后无需setTimeout触发scrollIntoView方案

问题
  • 有没有无需使用setTimeout就能触发el.scrollIntoView()的方法?SPA里用定时器不够优雅,之前在Angular和React的类似场景中都这么做过,想找更优方案。
  • 发现当已有容器处于展开状态时,重渲染会改变所有元素的Y轴位置,导致点击下一个容器时出现过度滚动。
  • 是否应该用useMemo()存储ref的初始位置?

解决方案

完全可以去掉setTimeout,核心是要等到Accordion展开/收起的DOM布局更新完成后再执行滚动,以下是两种可靠方案:

方案一:监听Accordion过渡结束事件(推荐)

Material-UI的Accordion自带过渡动画,监听transitionend事件能精准捕捉布局稳定的时机,避免定时器的不确定性:

修改滚动逻辑

const [expanded, setExpanded] = React.useState<string | false>(false);
const watchedSelections = useSelector(getWatchedSelections);
const {refs, setRefFromKey} = useMultipleRefs();
const {height} = useViewPort();

const handleChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
  const targetPanel = event.currentTarget as HTMLElement;
  setExpanded(isExpanded ? panel : false);
  
  // 监听过渡结束,执行滚动后移除事件监听
  const handleTransitionEnd = () => {
    targetPanel.scrollIntoView({ behavior: "smooth", block: "start" });
    targetPanel.removeEventListener('transitionend', handleTransitionEnd);
  };
  
  targetPanel.addEventListener('transitionend', handleTransitionEnd);
};

// 移除原有的elRef状态和带setTimeout的useLayoutEffect

调整JSX结构

不需要再通过onClick存储元素信息,直接在onChange中处理:

return (
  <Accordion
    key={selection.tag}
    ref={setRefFromKey(selection.tag)}
    id={selection.tag}
    TransitionProps={{ unmountOnExit: false }} 
    expanded={expanded === `${selection.tag}`} 
    onChange={handleChange(selection.tag)}>
    <AccordionSummary
      aria-controls={`${selection.tag}-content`}
      id={`${selection.tag}-header`}
      className='scroll-ref'
      sx={selectionSummaryStyling}
    >
      <SelectionsListItem selection={selection} />
    </AccordionSummary>
    
    <AccordionDetails>
      <Typography>
        //...content with 380px height here.
      </Typography>
    </AccordionDetails>
  </Accordion>
)

方案二:用useLayoutEffect监听状态变化(无过渡场景)

如果Accordion没有过渡动画,useLayoutEffect会在DOM更新完成后同步执行,比setTimeout更可靠:

useLayoutEffect(() => {
  if (expanded) {
    // 直接通过ref获取元素,无需document.getElementById
    const targetEl = refs[expanded];
    targetEl?.scrollIntoView({ behavior: "smooth", block: "start" });
  }
}, [expanded, refs]);

关于过度滚动和useMemo的问题

  • 过度滚动的根源是布局未稳定时就执行了滚动,上面两种方案都是在布局完全更新后触发滚动,能直接解决该问题。
  • 不需要用useMemo存储初始位置:初始位置会随着容器展开/收起失效,反而会导致滚动位置不准确,直接在布局稳定后获取实时位置即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 21:23:11