Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which one has better performance: add and remove event listener on every render VS running an useEffect to update a ref

Here's my situation:

I've got a custom hook, called useClick, which gets an HTML element and a callback as input, attaches a click event listener to that element, and sets the callback as the event handler.

App.js

function App() {
  const buttonRef = useRef(null);
  const [myState, setMyState] = useState(0);

  function handleClick() {
    if (myState === 3) {
      console.log("I will only count until 3...");
      return;
    }
    setMyState(prevState => prevState + 1);
  }

  useClick(buttonRef, handleClick);

  return (
    <div>
      <button ref={buttonRef}>Update counter</button>
      {"Counter value is: " + myState}
    </div>
  );
}

useClick.js

import { useEffect } from "react";

function useClick(element, callback) {
  console.log("Inside useClick...");

  useEffect(() => {
    console.log("Inside useClick useEffect...");
    const button = element.current;

    if (button !== null) {
      console.log("Attaching event handler...");
      button.addEventListener("click", callback);
    }
    return () => {
      if (button !== null) {
        console.log("Removing event handler...");
        button.removeEventListener("click", callback);
      }
    };
  }, [element, callback]);
}

export default useClick;

Note that with the code above, I'll be adding and removing the event listener on every call of this hook (because the callback, which is handleClick changes on every render). And it must change, because it depends on the myState variable, that changes on every render.

And I would very much like to only add the event listener on mount and remove on dismount. Instead of adding and removing on every call.


Here on SO, someone have suggested that I coulde use the following:

useClick.js

function useClick(element, callback) {
    console.log('Inside useClick...');

    const callbackRef = useRef(callback);

    useEffect(() => {
        callbackRef.current = callback;
    }, [callback]);

    const callbackWrapper = useCallback(props => callbackRef.current(props), []);

    useEffect(() => {
        console.log('Inside useClick useEffect...');
        const button = element.current;

        if (button !== null) {
            console.log('Attaching event handler...');
            button.addEventListener('click', callbackWrapper);
        }
        return () => {
            if (button !== null) {
                console.log('Removing event handler...');
                button.removeEventListener('click', callbackWrapper);
            }
        };
    }, [element, callbackWrapper]);
}

QUESTION

It works as intended. It only adds the event listener on mount, and removes it on dismount.

The code above uses a callback wrapper that uses a ref that will remain the same across renders (so I can use it as the event handler and mount it only once), and its .current property it's updated with the new callback on every render by a useEffect hook.

The question is: performance-wise, which approach is the best? Is running a useEffect() hook less expensive than adding and removing event listeners on every render?

Is there anyway I could test this?

like image 293
cbdeveloper Avatar asked May 21 '19 10:05

cbdeveloper


People also ask

Should you always remove event listeners?

The event listeners need to be removed due to following reason. Avoid memory leaks, if the browser is not handled it properly. Modern browsers will garbage collect event handlers of removed DOM elements but it is not true in cases of legacy browses like IE which will create memory leaks.

Should I remove event listeners before removing elements?

Removing the event listener first always results in lower memory usage (no leaks).

Which is the correct method to add an event listener?

The addEventListener() method allows you to add event listeners on any HTML DOM object such as HTML elements, the HTML document, the window object, or other objects that support events, like the xmlHttpRequest object.

What happens if you don't remove event listeners?

The main reason you should remove event listeners before destroying the component which added them is because once your component is gone, the function that should be executed when the event happens is gone as well (in most cases) so, if the element you bound the listener to outlasts the component, when the event ...


1 Answers

App.js

function App() {
  const buttonRef = useRef(null);
  const [myState, setMyState] = useState(0);

  // handleClick remains unchanged
  const handleClick = useCallback(
    () => setMyState(prevState => prevState >= 3 ? 3 : prevState + 1),
    []
  );

  useClick(buttonRef, handleClick);

  return (
    <div>
      <button ref={buttonRef}>Update counter</button>
      {"Counter value is: " + myState}
    </div>
  );
}

A more professional answer:

App.js

function App() {
  const buttonRef = useRef(null);
  const [myState, handleClick] = useReducer(
    prevState => prevState >= 3 ? 3 : prevState + 1,
    0
  );

  useClick(buttonRef, handleClick);

  return (
    <div>
      <button ref={buttonRef}>Update counter</button>
      {"Counter value is: " + myState}
    </div>
  );
}
like image 195
dancerphil Avatar answered Oct 02 '22 13:10

dancerphil