In React, I have a number of buttons (imagine a PIN layout with numbers) that update the state on click. I also added an event listener to the document
so pressing keys on the keyboard updates the pin too. However, there's a strange problem. When I add a number by clicking a button, the state is working correctly and everything is fine, but when I press a key on a physical keyboard, the state updates, but logs as <empty string>
!
Here is the code:
export default function Keypad() {
const [pin, setPin] = useState("");
function addNumber(num) {
console.log(pin); // returns the correct pin with handleKeyClick, returns <empty string> with handleKeyDown
if (pin.length < 6) { // only works if the pin is not <empty string>
setPin((pin) => [...pin, num.toString()]); // works correctly with both handleKeyClick and handleKeyDown even if pin logged <empty string>!
}
}
function handleKeyClick(num) {
addNumber(num);
}
function handleKeyDown(e) {
if (!isNaN(e.key)) {
addNumber(e.key);
}
}
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<div>
{/* just one button for example */}
<button onClick={() => handleKeyClick(9)}>9</button>
</div>
)
}
I guess this is because document
can't access the pin
state, but if it was the case, the setPin
shouldn't work either. Am I right?
Your component does not keep a reference when listening to DOM events, this answer has some neat code for listening to window events using a fairly simple hook. When applied to your code, it works as expected:
const {useState, useEffect, useRef} = React;
// Hook
function useEventListener(eventName, handler, element = window){
// Create a ref that stores handler
const savedHandler = useRef();
// Update ref.current value if handler changes.
// This allows our effect below to always get latest handler ...
// ... without us needing to pass it in effect deps array ...
// ... and potentially cause effect to re-run every render.
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(
() => {
// Make sure element supports addEventListener
// On
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// Create event listener that calls handler function stored in ref
const eventListener = event => savedHandler.current(event);
// Add event listener
element.addEventListener(eventName, eventListener);
// Remove event listener on cleanup
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] // Re-run if eventName or element changes
);
};
const Keypad = (props) => {
const [pin, setPin] = useState([]);
function addNumber(num) {
console.log(pin); // returns the correct pin with handleKeyClick, returns <empty string> with handleKeyDown
if (pin.length < 6) { // only works if the pin is not <empty string>
setPin((pin) => [...pin, num.toString()]); // works correctly with both handleKeyClick and handleKeyDown even if pin logged <empty string>!
}
}
function handleKeyClick(num) {
addNumber(num);
}
function handleKeyDown(e) {
if (!isNaN(e.key)) {
addNumber(e.key);
}
}
useEventListener("keydown", handleKeyDown)
return (
<div>
{/* just one button for example */}
<button onClick={() => handleKeyClick(9)}>9</button>
</div>
)
return "Hello World"
}
ReactDOM.render(<Keypad />, document.getElementById("root"))
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>