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

如何让React在处理并发鼠标事件时强制重新渲染?

解决React嵌套元素鼠标事件竞态问题

问题描述

组件包含两层嵌套<div>,通过useState存储鼠标位置的「深度」(外部为0,外层div内为1,内层div内为2),使用useCallback包装的onMouseEnter/onMouseLeave事件处理函数来增减深度值。

快速将鼠标从内层div移到外部时,React可能在两次事件触发间不重新渲染,导致两个onMouseLeave事件使用同一个旧的state值执行,出现竞态条件,最终深度值错误停留在1。

复现代码:

function App() {
  // console.log("---RENDER---");
  
  const [value, setValue] = React.useState(0);

  const onMouseEnter = React.useCallback(() => {
    // console.log("   incr");
    setValue(value+1);
  }, [ value ]);
  const onMouseLeave = React.useCallback(() => {
    // console.log("   decr");
    setValue(value-1);
  }, [ value ]);
  const events = { onMouseEnter, onMouseLeave };
  
  return (
    <div>
      <h1>{value}</h1>
      <div style={{"background": "blue", "padding": "1rem"}} {...events}>
        <div style={{"background": "red", "width": "2rem", "height": "2rem"}} {...events}>
        </div>
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root" />

基础场景修复:函数式更新

原代码的问题在于useCallback依赖value,快速触发事件时,两次onMouseLeave会捕获同一个旧的value值,导致两次递减操作都基于同一个初始值计算。

使用函数式更新替代直接依赖state的更新方式,React会自动传入当前最新的state值作为参数,确保每次更新都基于最新状态:

function App() {
  const [value, setValue] = React.useState(0);

  const onMouseEnter = React.useCallback(() => {
    setValue(prev => prev + 1);
  }, []);
  const onMouseLeave = React.useCallback(() => {
    setValue(prev => prev - 1);
  }, []);
  const events = { onMouseEnter, onMouseLeave };
  
  return (
    <div>
      <h1>{value}</h1>
      <div style={{background: "blue", padding: "1rem"}} {...events}>
        <div style={{background: "red", width: "2rem", height: "2rem"}} {...events}>
        </div>
      </div>
    </div>
  );
}

这种方式无需维护额外队列,完全利用React内置的state更新机制解决竞态问题。

复杂场景解决方案(依赖事件参数+更新后state)

如果事件处理函数需要执行复杂耗时逻辑,且依赖事件源参数和更新后的state值,可以通过以下两种方式实现:

方案1:函数式更新内直接处理逻辑

在函数式更新的回调中,既可以获取最新的prev state计算新值,也能直接使用事件参数执行逻辑:

function App() {
  const [value, setValue] = React.useState(0);

  const onMouseEnter = React.useCallback(() => {
    setValue(prev => prev + 1);
  }, []);
  
  const onMouseLeave = React.useCallback((e) => {
    // 获取事件源参数
    const targetId = e.target.id;
    // 函数式更新中处理依赖最新state的逻辑
    setValue(prev => {
      const newValue = prev - 1;
      // 执行复杂耗时逻辑,同时使用新状态和事件参数
      handleComplexLogic(newValue, targetId);
      return newValue;
    });
  }, []);

  const handleComplexLogic = (newValue, targetId) => {
    // 模拟异步耗时操作
    setTimeout(() => {
      console.log(`更新后深度值:${newValue},触发事件的元素ID:${targetId}`);
    }, 100);
  };

  const events = { onMouseEnter, onMouseLeave };
  
  return (
    <div>
      <h1>{value}</h1>
      <div id="outer-div" style={{background: "blue", padding: "1rem"}} {...events}>
        <div id="inner-div" style={{background: "red", width: "2rem", height: "2rem"}} {...events}>
        </div>
      </div>
    </div>
  );
}

方案2:使用useReducer统一管理状态与逻辑

将状态更新和复杂逻辑封装到reducer中,事件处理函数仅负责分发包含事件参数的action,确保状态更新和逻辑执行的顺序性:

// 处理状态更新与复杂逻辑的reducer
function depthReducer(state, action) {
  switch(action.type) {
    case 'INCREMENT':
      return { ...state, value: state.value + 1 };
    case 'DECREMENT':
      const newValue = state.value - 1;
      // 利用action中的事件参数执行逻辑
      handleComplexLogic(newValue, action.payload.targetId);
      return { ...state, value: newValue };
    default:
      return state;
  }
}

function handleComplexLogic(newValue, targetId) {
  setTimeout(() => {
    console.log(`更新后深度值:${newValue},触发事件的元素ID:${targetId}`);
  }, 100);
}

function App() {
  const [state, dispatch] = React.useReducer(depthReducer, { value: 0 });

  const onMouseEnter = React.useCallback(() => {
    dispatch({ type: 'INCREMENT' });
  }, []);
  
  const onMouseLeave = React.useCallback((e) => {
    dispatch({ 
      type: 'DECREMENT', 
      payload: { targetId: e.target.id } 
    });
  }, []);

  const events = { onMouseEnter, onMouseLeave };
  
  return (
    <div>
      <h1>{state.value}</h1>
      <div id="outer-div" style={{background: "blue", padding: "1rem"}} {...events}>
        <div id="inner-div" style={{background: "red", width: "2rem", height: "2rem"}} {...events}>
        </div>
      </div>
    </div>
  );
}

这两种方案都避免了手动维护队列的复杂度,同时保证了事件逻辑的正确性和顺序性。

内容的提问来源于stack exchange,提问作者Arthur F.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 02:35:59