如何防止React组件重渲染时滚动位置被重置
自定义列筛选弹窗操作后滚动条自动回到顶部问题解决方案
核心原因
出现该问题的本质是勾选/取消筛选选项时触发状态更新,导致自定义弹窗DOM被销毁重挂载,而非正常的属性更新重渲染,常见触发场景包括:
- 将自定义弹窗的组件定义直接写在Data Grid的插槽(slots)配置、父组件render函数内部,每次表格状态变更(筛选值修改属于Data Grid受控状态更新),插槽函数重新执行,弹窗DOM被销毁后重建,浏览器默认将新生成的滚动容器定位到顶部。
- 给弹窗组件配置了动态变化的
key属性,每次渲染key值变动触发组件强制重挂载。 - 传给弹窗的事件回调没有做引用固定,每次父组件渲染生成新的函数引用,触发子组件意外卸载重载。
优先方案:从渲染逻辑层面规避问题(无需手动维护滚动位置)
这是最彻底的解决方式,从根源上避免弹窗无意义重挂载:
- 将自定义筛选弹窗的组件定义抽离到Data Grid父组件的渲染逻辑外层,不要在slots配置项、父组件return的JSX中内联定义弹窗组件。
- 所有传给弹窗的事件回调(勾选状态变更、筛选确认/重置等)用
useCallback包裹,确保多次渲染间函数引用稳定。 - 移除弹窗组件上不必要的动态key配置,避免key值变动触发强制重挂载。
参考实现代码:
// 将自定义弹窗组件抽离到父组件外部定义,不要嵌套写在父组件内部 const CustomColumnFilterPanel = React.forwardRef((props, ref) => { // 弹窗内部筛选逻辑、选项渲染逻辑 return ( <div ref={ref} className="custom-filter-panel"> {/* 筛选选项列表 */} </div> ) }); function DataGridPage() { // 用useCallback固定事件回调引用 const handleFilterValueChange = useCallback((newFilterModel) => { setTableFilterModel(newFilterModel); }, []); return ( <DataGrid filterModel={tableFilterModel} slots={{ // 直接引用外层定义的组件,禁止在这里内联写组件定义 columnsPanel: CustomColumnFilterPanel }} slotProps={{ columnsPanel: { onFilterChange: handleFilterValueChange } }} /> ) }
兜底方案:重渲染后自动恢复滚动位置
如果受业务逻辑限制,无法完全避免弹窗重挂载,可以通过手动记录、恢复滚动位置实现无感知体验:
- 给弹窗的滚动容器绑定DOM ref。
- 监听滚动容器的
scroll事件,将实时滚动位置存在非响应式ref中,不要用普通state存储滚动值,避免触发循环重渲染。 - 在组件每次挂载、更新完成后的
useEffect执行时机,将存储的滚动值重新赋值给滚动容器,恢复之前的浏览位置。
参考实现代码:
const CustomFilterPanel = (props) => { const scrollContainerRef = useRef(null); // 用ref存储滚动位置,不会触发组件重渲染 const savedScrollTop = useRef(0); // 组件每次更新/挂载后恢复滚动位置 useEffect(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTop = savedScrollTop.current; } }); const handleContainerScroll = useCallback((e) => { savedScrollTop.current = e.target.scrollTop; }, []); return ( <div ref={scrollContainerRef} onScroll={handleContainerScroll} style={{ maxHeight: 320, overflowY: 'auto' }} > {/* 筛选选项内容 */} </div> ) }
注意:手动恢复滚动位置属于兼容兜底逻辑,性能和体验弱于从渲染结构层面解决重挂载问题,优先推荐使用第一种方案。
内容的提问来源于stack exchange,提问作者Caleb Koch
相关产品推荐
相关产品推荐

