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
相关产品推荐
相关产品推荐

