React Hooks函数组件避免重渲染咨询:解决滚动位置重置问题
解决React Hooks组件悬停导致重渲染与滚动位置丢失问题
我来帮你拆解问题根源并给出落地的修复方案:
你的核心痛点是每次鼠标悬停/离开单元格时,父组件的removeDisplayData state发生更新,触发了整个PlayerCalendar组件的重渲染——这不仅会让所有单元格无差别重新渲染,还会导致滚动容器重置滚动位置,严重影响操作体验。下面是针对性的解决思路:
1. 拆分独立子组件 + React.memo精准控制重渲染
把单个单元格的渲染逻辑抽成独立子组件,并用React.memo包裹。它会对组件的props做浅比较,只有当props真正变化时才会触发子组件重渲染,避免父组件一更新就带动所有单元格刷新。
import React, { useState, useCallback } from 'react' import moment from 'moment' type removeDisplayItem = { dataId: number; display: boolean } type scheduleItem = { id: number; startTime: string; endTime: string } // 抽离独立子组件,用React.memo做props浅比较 const ScheduleCell = React.memo(({ data, removeDisplayData, handleDisplayRemove, setCalendarRemoveItemConfirm }: { data: scheduleItem removeDisplayData: removeDisplayItem handleDisplayRemove: (dataId: number, display: boolean) => void setCalendarRemoveItemConfirm: (value: boolean) => void }) => { return ( <> <div className="cell-content-wrapper" style={removeDisplayData.dataId === data.id && removeDisplayData.display ? { display: "none" } : { display: "flex" }} onMouseOver={() => handleDisplayRemove(data.id, true)} > <div className="start-time-text">{moment(data.startTime).format("HH:mm")}</div> <div className="dash"> - </div> <div className="end-time-text">{moment(data.endTime).format("HH:mm")}</div> </div> <div className="cell-content-wrapper" onMouseLeave={() => handleDisplayRemove(data.id, false)} style={ removeDisplayData.dataId === -1 ? { display: "none" } : removeDisplayData.dataId === data.id && removeDisplayData.display ? { display: "flex", justifyContent: "space-between", backgroundColor: "#8c9296", color: "white", cursor: "pointer" } : { display: "none" } } onClick={() => setCalendarRemoveItemConfirm(true)} > <div className="dash"> </div> <div className="start-time-text">Remove</div> <div className="end-time-text"><i className="fas fa-trash-alt"></i></div> </div> </> ) }) const PlayerCalendar = () => { const [removeDisplayData, setRemoveDisplayData] = useState<removeDisplayItem>({ dataId: -1, display: false }) const [calendarRemoveItemConfirm, setCalendarRemoveItemConfirm] = useState(false) // 用useCallback缓存事件函数,避免父组件每次渲染生成新函数触发子组件重渲染 const handleDisplayRemove = useCallback((dataId: number, display: boolean) => { setRemoveDisplayData({ dataId, display }) }, []) return ( <div className="scroll-container"> {/* 你的滚动容器 */} {/* 假设exampleData是数组,循环渲染;单个数据直接传入即可 */} {exampleData.map(item => ( <ScheduleCell key={item.id} data={item} removeDisplayData={removeDisplayData} handleDisplayRemove={handleDisplayRemove} setCalendarRemoveItemConfirm={setCalendarRemoveItemConfirm} /> ))} </div> ) } export default PlayerCalendar;
2. 进阶优化:把状态下放到子组件(彻底避免父组件重渲染)
如果你的需求只是控制单个单元格的悬停显示逻辑,完全可以把状态下放到子组件内部,让每个单元格自己管理悬停状态。这样父组件完全不需要更新state,从根源上杜绝重渲染问题:
const ScheduleCell = React.memo(({ data, setCalendarRemoveItemConfirm }: { data: scheduleItem setCalendarRemoveItemConfirm: (value: boolean) => void }) => { // 子组件自己管理悬停状态 const [isHovered, setIsHovered] = useState(false) return ( <> <div className="cell-content-wrapper" style={isHovered ? { display: "none" } : { display: "flex" }} onMouseOver={() => setIsHovered(true)} > <div className="start-time-text">{moment(data.startTime).format("HH:mm")}</div> <div className="dash"> - </div> <div className="end-time-text">{moment(data.endTime).format("HH:mm")}</div> </div> <div className="cell-content-wrapper" onMouseLeave={() => setIsHovered(false)} style={isHovered ? { display: "flex", justifyContent: "space-between", backgroundColor: "#8c9296", color: "white", cursor: "pointer" } : { display: "none" }} onClick={() => setCalendarRemoveItemConfirm(true)} > <div className="dash"> </div> <div className="start-time-text">Remove</div> <div className="end-time-text"><i className="fas fa-trash-alt"></i></div> </div> </> ) })
这种方案效率最高,因为悬停状态完全由子组件自治,父组件不会有任何state更新,滚动位置自然不会丢失。
3. 兜底方案:保存并恢复滚动位置(若必须更新父组件state)
如果业务需求强制要在父组件管理状态,那可以通过监听滚动事件保存位置,在组件更新后恢复:
const PlayerCalendar = () => { const scrollRef = useRef<HTMLDivElement>(null) const [scrollTop, setScrollTop] = useState(0) // 监听滚动,保存当前位置 const handleScroll = useCallback(() => { if (scrollRef.current) { setScrollTop(scrollRef.current.scrollTop) } }, []) // 组件更新后恢复滚动位置 useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollTop } }, [scrollTop]) return ( <div className="scroll-container" ref={scrollRef} onScroll={handleScroll} > {/* 你的单元格内容 */} </div> ) }
优先推荐前两种方案,尤其是第二种,从根源上解决重渲染问题,体验最优。
内容的提问来源于stack exchange,提问作者Emre Sert
相关产品推荐
相关产品推荐

