0

Here is a basic example of the issue. A component contains two nested divs. We can keep track of the "depth" of the mouse position (0 if outside both divs, 1 if inside the outer div but outside the inner div, 2 if inside the inner div) by using useState to store the current depth and creating two useCallback-ed event handlers, one which increments the depth value and one which decrements it (then attaching these event handlers appropriately).

This mostly works fine, but if we move the mouse as fast as we can from inside the inner div to the outside, sometimes React fails to rerender the component between the firing of the events, so both copies of the decrement event handler run within the same render, and we have a race condition -- the component is left thinking the depth value is 1. The snippet below showcases exactly this (move your mouse slowly into the blue then very fast out back to the white).

What's the best way to modify the situation to prevent this? My current best idea is to add a queue (of increments/decrements) to the component and some kind of useEffect(..., [ queue ]) to guarantee processing the queue one operation at a time. It would be nice if anyone can offer something simpler/more ergonomic (in the real application, operations are obviously more complex than just increments/decrements, so it would be nice to avoid the complexity of queueing these unless totally necessary). Ideally I would just like to be able to tell the component to take over control of (mouse) event dispatch on its children and buffer these appropriately.

I can't find any fixes through Googling. The best match I can find is here but this is from 2019 so very possibly outdated and the main link is dead.

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" />

EDIT: As @SergeySosunov pointed out, the bug in the snippet can be fixed by using the functional update form of setState. But imagine that the event handler for onMouseLeave then needs to perform some (complicated, expensive) piece of logic which depends on additional parameters passed from the event source, and also depends on the value of state immediately after the update. Because of the dependence on additional parameters of the specific event, we can't just pass down responsibility for this additional piece of logic to some kind of useEffect(..., [ state ]) hook, because then we lose information about what caused the change to state. We also cannot perform the logic immediately, because as evidenced by the snippet, we don't necessarily know the true value of state.

5
  • 2
    Have you tried functional update from useState hook? setValue(curr => curr - 1)? Commented Jul 8 at 19:48
  • @SergeySosunov Ah, yes, duh. That solves the immediate issue. For the sake of the actual application I would still be interested in a (partial) answer to the "spirit" of my original question - I will try to make an edit that conveys the subtlety...
    – Arthur F.
    Commented Jul 8 at 19:57
  • @SergeySosunov answer is the full answer. This is due to closures and the speed at which the event is called. The event is triggered before React has had a chance to rerender which means no new functions with new closures have been created. The events are still processed in the order they are called (no need for a queue) you just need to get the actual value of the state not the one contained by the closure and this is done by the functional update.
    – Jacob Smit
    Commented Jul 9 at 5:34
  • @JacobSmit Did you see my edit? Because the functional update needs to be pure, I don't see a way to use it to perform additional logic depending on the state value, which is needed here. I understand the reason for the behaviour. I am just wondering if anyone has run into the issue before (it is a real limitation, not addressed solely by functional updates) and knows of a solution. E.g. it seems feasible to me that with sufficient magic done on DOM rendering, React could "take control" of event dispatch within components to ensure rerenders happen between every pair of events.
    – Arthur F.
    Commented Jul 9 at 7:21
  • It would be possible to see if you were over complicating the issue if you actually provide code showing the problem not half of it. useReducer create your state, create your actions to update state, you should be able to make this do what you are asking for (if I understand correctly). Or you can step outside react and create an object/class that can handle this and send updates via subscribe / unsubscribe functionality.
    – Jacob Smit
    Commented Jul 9 at 21:36

0