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

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"> &nbsp;</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"> &nbsp;</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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 09:52:43