Search code examples
reactjsstatesetstateuse-effect

How to execute change in a component without setting state in a react app?


The component below Will set state if right or left arrow key is pressed. because each pressing key executes two set states, it will leads some performance issues. How can I get change in pressing keys without setting states or with just one set state?

import { useState, useEffect } from "react";


export function useKeyPress(targetKey) {
  const [keyPressed, setKeyPressed] = useState(false)
  
  function downHandler({ key }) {
    if (key === targetKey) {
      setKeyPressed(true);
    }
  }
  
  const upHandler = ({ key }) => {
    if (key === targetKey) {
      setKeyPressed(false);
    }
  }
  
  useEffect(() => {
    window.addEventListener("keydown", downHandler);
    window.addEventListener("keyup", upHandler);
    
    return () => {
      window.removeEventListener("keydown", downHandler);
      window.removeEventListener("keyup", upHandler);
    }
  })
  
  return keyPressed
}

Solution

  • If you always need the current value of keyPressed, there's no getting around setting state, but you can skip the effect by specifying the dependency list and calling useRef() like this:

    export function useEventListener(targetRef, type, handler) {
      const handlerRef = useRef(null);
    
      useEffect(() => {
        handlerRef.current = handler;
      }, [handler]);
    
      useEffect(() => {
        const target = targetRef.current;
        const listener = (event) => handlerRef.current(event);
    
        target.addEventListener(type, listener);
    
        return () => {
          target.removeEventListener(type, listener);
        };
      }, [targetRef, type]);
    }
    

    And then you can define your function like this:

    export function useKeyPress(targetKey) {
      const [keyPressed, setKeyPressed] = useState(false);
      const windowRef = useRef(window);
    
      useEventListener(windowRef, 'keydown', ({ key }) => {
        if (key === targetKey) {
          setKeyPressed(true);
        }
      });
    
      useEventListener(windowRef, 'keyup', ({ key }) => {
        if (key === targetKey) {
          setKeyPressed(false);
        }
      });
    
      return keyPressed;
    }
    

    While the targetRef parameter may seem roundabout at first in the particular example above, it's very useful for instance when you want to add event listeners to JSX elements like this:

    const { useEffect, useRef } = React;
    
    function useEventListener(targetRef, type, handler) {
      const handlerRef = useRef(null);
    
      useEffect(() => {
        handlerRef.current = handler;
      }, [handler]);
    
      useEffect(() => {
        const target = targetRef.current;
        const listener = (event) => handlerRef.current(event);
    
        target.addEventListener(type, listener);
    
        return () => {
          target.removeEventListener(type, listener);
        };
      }, [targetRef, type]);
    }
    
    function App() {
      const buttonRef = useRef(null);
    
      useEventListener(buttonRef, 'click', () => {
        console.log('button clicked');
      });
    
      return (
        <button ref={buttonRef}>Click Me</button>
      );
    }
    
    ReactDOM.render(<App />, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="root"></div>