skip to Main Content

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 ?

2

Answers


  1. Your effect has a dependency on the handleMouseOverNode as it uses that function within it, so you should provide handleMouseOverNode as a dependency in the dependency array:

    const [otherState, setOtherState] = useState()
    const handleMouseOverNode = (e) => {
      // should now be the latest state value
      console.log (otherState);
    }
    useEffect(() => {
      cyRef.current.on('mouseover', 'node', handleMouseOverNode);
    
      return () => {
        cyRef.current.removeListener('mouseover');
      };
    }, [cyRef.current, handleMouseOverNode]);
    

    With this approach, your useEffect will recreate the event listener on every rerender, giving it access to call the newest handleMouseOverNode function that has access to the latest state. I suggest that you optimize this though, as a new function reference will be created on every rerender with the above approach. Instead, you can memoize the callback function so that it’s only recreated (and thus your even listeners are only re-added) when your function state changes by using useCallback():

    const handleMouseOverNode = useCallback((e) => {
      // this only gets the initial value but doesn't update when state changes
      console.log (otherState)
    }, [otherState]);
    

    And then pass handleMouseOverNode as a dependency to the useEffect() as done above.

    Login or Signup to reply.
  2. 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) => {
      // 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]); // 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]);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search