Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Event listener functions changing when using React hooks

I have a component that uses event listeners in several places through addEventListener and removeEventListener. It's not sufficient to use component methods like onMouseMove because I need to detect events outside the component as well.

I use hooks in the component, several of which have the dependency-array at the end, in particular useCallback(eventFunction, dependencies) with the event functions to be used with the listeners. The dependencies are typically stateful variables declared using useState.

From what I can tell, the identity of the function is significant in add/remove EventListener, so that if the function changes in between it doesn't work. At first i tried managing the hooks so that the event functions didn't change identity between add and remove but that quickly became unwieldy with the functions' dependency on state.

So in the end I came up with the following pattern: Since the setter-function (the second input parameter to useState) gets the current state as an argument, I can have event functions that never change after first render (do we still call this mount?) but still have access to up-to-date stateful variables. An example:

import React, { useCallback, useEffect, useState } from 'react';

const Component = () => {
  const [state, setState] = useState(null);

  const handleMouseMove = useCallback(() => {
    setState((currentState) => {
      // ... do something that involves currentState ...
      return currentState;
    });
  }, []);

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, [/* ... some parameters here ... */]);

  // ... more effects etc ...

  return <span>test</span>;
};

(This is a much simplified illustration).

This seems to work fine, but I'm not sure if it feels quite right - using a setter function that never changes the state but just as a hack to access the current state.

Also, for event functions that require several state variables I have to nest the setter calls.

Is there another pattern that could handle this situation in a nicer way?

like image 251
jorgen Avatar asked Sep 30 '19 07:09

jorgen


2 Answers

From what I can tell, the identity of the function is significant in add/remove EventListener, so that if the function changes in between it doesn't work.

While this is true, we do not have to go the extreme of arranging for even function not to change identity at all.

Simple steps will be: Declare event function using useCallback - dependency list of useCallback should include all the stateful variables that your function depends on.

Use useEffect to add the event listener. Return cleanup function that will remove the event listener. Dependency list of useEffect should include the event listener function itself, in addition to any other stateful variable your effect function might be using.

This way, when any of stateful variable used by event listener changes, even listener's identity changes, which will trigger running of the effect, but before running the effect cleanup function returned by previous run of the effect will be run, properly removing old event listener before adding the new one.

Something on the lines of:

const Component = () => {
    const [state, setState] = useState();

    const eventListner = useCallback(() => {
        console.log(state); // use the stateful variable in event listener
    }, [state]);

    useEffect(() => {
        el.addEventListner('someEvent', eventListner);
        return () => el.removeEventListener('someEvent', eventListner);
    }, [eventListener]);
}
like image 89
ckedar Avatar answered Sep 21 '22 06:09

ckedar


@ckedar 's solution can solve this question, but it has performance problem, when the eventListener change, react will remove and addEvent on the dom。

you can use useRef() instead useState(),if you want listen state change, you can use useStateRef():

import React, { useEffect, useRef, useState } from 'react';

export default function useStateRef(initialValue:any): Array<any>{
  const [value, setValue] = useState(initialValue);
  const ref = useRef(value);
  useEffect(() => {
    ref.current = value;
  },[value])
  return [value,setValue,ref];
}
like image 36
water Avatar answered Sep 18 '22 06:09

water