How to run a function in React when the Escape button has been pressed?

Viewed 483

The whole idea of the component is that the first time the Escape key is pressed, the value confirm in state changes to true to verify that the user really wants to execute the secPress() function and pressing the Escape key again within 5 seconds run the secPress() function.

Unfortunately, the secPress() function in the component never run.

How to get the function to run after the second key press?

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

export function useKeypress(key, action) {
  useEffect(() => {
    function onKeyup(e) {
      if (e.key === key) action();
    }
    window.addEventListener("keyup", onKeyup);
    return () => window.removeEventListener("keyup", onKeyup);
  }, []);
}

const App = () => {
  const [confirm, setConfirm] = useState(false);

  // this function never run
  const secPress = () => {
    console.log("Double escape pressed");
  };

  const _confirm = useCallback(() => {
    setConfirm((prev) => !prev);
    console.log(confirm); // <-- always show false
    if (confirm) {
      secPress();
    }
  }, [confirm]);

  useKeypress("Escape", _confirm);

  useEffect(() => {
    const d = setTimeout(() => {
      setConfirm(false);
    }, 5000);
    return () => {
      clearTimeout(d);
    };
  }, [confirm]);

  return <>{confirm ? "true" : "false"}</>;
};

export default App;

React version: 17.0.2

2 Answers

In your case, it's because you're passing an empty dependencies array to the useEffect inside the useKeypress function; this way, the event listener has always the same callback.

The easiest change would be to pass the dependencies, for example:

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

export function useKeypress(key, action, deps) {
  useEffect(() => {
    function onKeyup(e) {
      if (e.key === key) action();
    }
    window.addEventListener("keyup", onKeyup);
    return () => window.removeEventListener("keyup", onKeyup);
  }, deps);
}

const App = () => {
  const [confirm, setConfirm] = useState(false);

  // this function never run
  const secPress = () => {
    console.log("Double escape pressed");
  };

  const _confirm = useCallback(() => {
    setConfirm(prev => !prev);
    console.log(confirm); // <-- always show false
    if (confirm) {
      secPress();
    }
  }, [confirm]);

  useKeypress("Escape", _confirm, [confirm]);

  useEffect(() => {
    const d = setTimeout(() => {
      setConfirm(false);
    }, 5000);
    return () => {
      clearTimeout(d);
    };
  }, [confirm]);

  return <>{confirm ? "true" : "false"}</>;
};

export default App;

The previous answer is incorrect, you shouldn't have to add [confirm] to the event listener effect

working example:

https://codesandbox.io/s/00rxv

  const [stage, setStage] = useState("initial");

  const secPress = () => {
    console.log("Double escape pressed");
  };

  const keydown = (e) => {
    if (e.key === "Escape") {
      setStage((current) => (current === "initial" ? "confirm" : "success"));
    }
  };

  useEffect(() => {
    window.addEventListener("keydown", keydown);

    return () => window.removeEventListener("keydown", keydown);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (stage === "success") {
      secPress();
    }
    if (stage !== "confirm") {
      return;
    }

    const d = setTimeout(() => {
      setStage("initial");
    }, 2000);
    return () => {
      clearTimeout(d);
    };
  }, [stage]);
Related