Search code examples
reactjsreact-hooksreact-statereact-state-management

Stale state inside an event handler


I have a JS library (Cytoscape) inside my React component. The library need to be handled events with set event handler functions (on diagram/ component initilazation).

The problem is that inside these events the state is stale - it only gets the first state because of the closure.

 const [otherState, setOtherState] = useState()
 const handleMouseOverNode = (e) => {
    // this only gets the initial value but doesn't update when state changes
    console.log (otherState)
 }
 useEffect(() => {
        cyRef.current.on('mouseover', 'node', handleMouseOverNode);
    }
    return () => {
        cyRef.current.removeListener('mouseover');
    };
 }, [cyRef.current]);

How do I get the real (non stale) state ?


Solution

  • The experimental useEffectEvent would solve this problem eventually. You can currently use it, but it's not stable, and might break in the future (see Declaring an Effect Event):

    import { experimental_useEffectEvent as useEffectEvent } from 'react';
    
    const [otherState, setOtherState] = useState()
    
    // wrap with useEffectEvent
    const handleMouseOverNode = useEffectEvent((e) => {
      console.log(otherState)
    })
    
    useEffect(() => {
      cyRef.current.on('mouseover', 'node', handleMouseOverNode);
    
      return () => {
        cyRef.current.removeListener('mouseover');
      };
    }, [cyRef.current]); // no need to add handleMouseOverNode as a dep
    

    Currently the clunky solution is to use another ref to hold the updated value:

    const [otherState, setOtherState] = useState()
    
    const otherStateRef = useRef(otherState) // create a ref
    
    // always update the ref with current value
    useEffect(() => {
      otherStateRef.current = otherState
    })
    
    useEffect(() => {
      const handleMouseOverNode = (e) => {
        console.log(otherStateRef.current) // get the value from the ref
      }
      
      cyRef.current.on('mouseover', 'node', handleMouseOverNode);
    
      return () => {
        cyRef.current.removeListener('mouseover');
      };
    }, [cyRef.current]);