使用useRef钩子滚动动态数量标签页时的异常问题
MUI Base Tabs 状态同步与滚动定位异常问题
复现步骤 🕹
组件源代码
import { useContext, useEffect, useRef } from 'react'; import * as React from 'react'; import { styled } from '@mui/system'; import Tabs from '@mui/base/Tabs'; import TabsList from '@mui/base/TabsList'; import TabPanel from '@mui/base/TabPanel'; import { buttonClasses } from '@mui/base/Button'; import Tab, { tabClasses } from '@mui/base/Tab'; import { Box, Typography } from '@mui/material'; import getDatesInMonth from '@/utils/getDatesInMonth'; import { dateContext } from '@/pages/mob'; import { useTabs } from '@mui/base'; export default function DayScrollView({ dates }) { const [activeDate, setActiveDate] = useContext(dateContext) const tabsRef = useRef(null); useEffect(() => { const activeTab = tabsRef.current.querySelector(`.${tabClasses.root}[aria-selected="true"]`); console.log(activeTab) if (activeTab) { activeTab.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center', }); } }, [activeDate, tabsRef]) return <Box sx={ { overflowX: 'scroll', scrollbarWidth: 'none', msOverflowStyle: 'none', '&::-webkit-scrollbar': { display: 'none', }, } }> <Tabs // ref={tabsRef} value={activeDate.toDateString()} onChange={(event, newValue) => { console.log('Event type:', event.type); console.log('Target element:', event.target); console.log(`Tab ${newValue} was clicked.`); console.log(event) if (activeDate.toDateString() !== newValue) setActiveDate(new Date(newValue)) }}> <StyledTabsList ref={tabsRef}> {dates.map((date, index) => { return ( <StyledTab // disableRipple={true} key={date.toDateString()} value={date.toDateString()} onChange={() => console.log('CLICKED ' + date.toDateString())}> <Typography variant='subtitle1'>{date.toLocaleString('default', { weekday: 'short' })}</Typography> <Typography variant='h5'>{date.getDate()}</Typography> </StyledTab> ) })} </StyledTabsList> </Tabs> </Box> } const StyledTab = styled(Tab)` color: #121212; cursor: pointer; ... 更多样式 `; const StyledTabsList = styled(TabsList)( ({ theme }) => ` padding-right: 1rem; ... 更多样式 `, );
使用该组件的父文件代码
import * as React from 'react'; import { AppbarDayScreen } from '@/components/mobile/home/Navbar'; import { useState } from 'react'; import { createContext } from 'react'; import DayScrollView from '@/components/mobile/home/DayScrollView'; import getDatesInMonth from '@/utils/getDatesInMonth'; const dateContext = createContext() export default function DayScreen() { const [activeDate, setActiveDate] = useState(new Date()); const [dates, setDates] = useState(getDatesInMonth(activeDate)) React.useEffect(() => { setDates(getDatesInMonth(activeDate)) }, [activeDate]) return ( <dateContext.Provider value={[activeDate, setActiveDate]}> <AppbarDayScreen /> <DayScrollView dates={dates} /> </dateContext.Provider> ); } export { dateContext }
当前行为 😯
- 当
activeDate变更时,Tabs组件的onChange回调函数不会触发 - 首次加载页面时,
useEffect里的activeTab能正确指向当前激活的标签页;但activeDate变更后,useEffect执行时,activeTab仍然指向之前的标签页,哪怕该标签页的aria-selected属性已经变为false
预期行为 🤔
tabsRef.current.querySelector(.${tabClasses.root}[aria-selected="true"]);应该能正确指向变更后的激活标签页,并且完成滚动定位。
上下文 🔦
我做了一个横向滚动的日期标签栏,点击导航栏的“月份”按钮可以选择日期,进而修改activeDate这个useState变量。activeDate会传递给标签组件,希望每次activeDate变更时,页面能自动滚动到对应的日期标签位置。
解决方案
问题核心是React状态更新后的DOM同步时机:当activeDate变更后,Tabs组件需要重新渲染来更新标签的aria-selected状态,但你的useEffect在activeDate变更后立即执行,此时DOM还未完成更新,所以查询到的还是旧的标签元素。
修复方案1:利用requestAnimationFrame等待DOM更新
把DOM查询逻辑放到requestAnimationFrame中,确保DOM完成更新后再执行滚动操作:
useEffect(() => { const updateScroll = () => { const activeTab = tabsRef.current.querySelector(`.${tabClasses.root}[aria-selected="true"]`); console.log(activeTab) if (activeTab) { activeTab.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center', }); } }; requestAnimationFrame(updateScroll); }, [activeDate])
修复方案2:直接通过日期值定位标签(更可靠)
不需要依赖aria-selected属性,直接通过标签的value属性定位目标元素,避免DOM状态同步问题:
useEffect(() => { const targetTabValue = activeDate.toDateString(); const activeTab = tabsRef.current.querySelector(`.${tabClasses.root}[value="${targetTabValue}"]`); if (activeTab) { activeTab.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center', }); } }, [activeDate])
额外说明
Tabs组件的onChange仅在用户手动点击标签时触发,通过修改value属性(比如从父组件传递activeDate)切换标签时,onChange不会执行,这是正常的设计逻辑。如果需要在activeDate变更时执行额外逻辑,应该放到监听activeDate的useEffect中,而非依赖Tabs的onChange。
内容的提问来源于stack exchange,提问作者Curious Learner
相关产品推荐
相关产品推荐

