4

I am creating a React application in which I have one state called input which is taking input from user. I want that when I press enter key, alert should display the input which is getting set in state. However, on clicking enter key, only default value set in input state is getting displayed.

But when I click on a button (which I have created to display the input in alert), then the input displayed in alert is correct.

Please see the code snippet below for reference:

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

export default function ShowAlertExample() {

  const [input, setInput ] = useState('1');
  const handleInputChange = (e) => {
    setInput(e.target.value);
  }

  const handleShowAlert = () => {
    alert(input);
  }

  const checkKeyPress = (e) =>{
    const { key, keyCode } = e;
    console.log(key, keyCode)
    if (keyCode === 13 ) {
      alert(input);
    }
  }

  useEffect(()=>{
    window.addEventListener("keydown", checkKeyPress)
    return ()=>{
      window.removeEventListener("keydown", checkKeyPress)
    }
  }, [])
  
  return (
    <div>
      <header className="App-header">
        <input value={input} onChange={handleInputChange} />
       <button onClick={handleShowAlert}>Show Alert</button>
      </header>
    </div>
  );
}

I made one change which can be seen in the code snippet below. I removed the [] from dependency list of useEffect() and it started working fine, but now the issue is it is adding and removing event listener each time I add something to input. This is not a good practice I guess if we are creating a big application.

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

export default function ShowAlertExample() {

  const [input, setInput ] = useState('1');
  const handleInputChange = (e) => {
    setInput(e.target.value);
  }

  const handleShowAlert = () => {
    alert(input);
  }

  const checkKeyPress = (e) =>{
    const { key, keyCode } = e;
    console.log(key, keyCode)
    if (keyCode === 13 ) {
      alert(input);
    }
  }

  useEffect(()=>{
    window.addEventListener("keydown", checkKeyPress)
    console.log("event listener added")
    return ()=>{
      console.log("event listener removed")
      window.removeEventListener("keydown", checkKeyPress)
    }
  })
  
  return (
    <div>
      <header className="App-header">
        <input value={input} onChange={handleInputChange} />
       <button onClick={handleShowAlert}>Show Alert</button>
      </header>
    </div>
  );
}

Is there anyway I can add event listener with the desired functionality without causing any performance issue. That is when I press Enter key, the alert should display the correct input. I have referred to the below Stack Overflow post and tried using useCallback() but it didn't work out.

https://stackoverflow.com/questions/55565444/how-to-register-event-with-useeffect-hooks#:~:text=hooks%20to%20below-,useEffect(()%20%3D%3E%20%7B%20window.,the%20effect%20once%2C%20imitating%20componentDidMount%20.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
satyam
  • 71
  • 1
  • 4
  • 1
    What is the purpose of `checkKeyPress` function? Perhaps if you explained your use-case we could suggest a more optimal solution. I.e. It is usually better to just attach an `onKeyDown` handler to the inputs that care about that event. This removes the stale enclosure issue. See [codesandbox demo](https://codesandbox.io/s/react-keypress-event-taking-only-initial-state-values-and-not-updated-values-j4ud7?file=/src/App.js) – Drew Reese Feb 15 '21 at 19:22
  • Hi @DrewReese - Thanks for your comment . My use case : I am making an application where I have provided keyboard shortcuts . When user clicks 'O' , a dialog box opens where there are radio buttons, checkboxes and text fields (pre-populated using some initial state) and a submit button . I want that when user presses enter key , I can use the data set in the states and form a JSON packet and submit them in POST request . Thats why I want to add checkKeyPress function to the event listener in useEffect() so that in checkKeyPress() , I can call submit() request . – satyam Feb 15 '21 at 19:39
  • 1
    Yes, you mention submitting a request and I agree with @Lakshya that perhaps a better solution would be to use a semantic form with `onSubmit` handler to access the form fields. If you store the form field values in component state then you'll always have the issue of needing to re-enclose current state values in any callbacks. The alternative to state is to use a React ref per input/field and access the current value when you need to. – Drew Reese Feb 15 '21 at 19:52
  • Drew and Lakshya - Thanks to both for quick replies . One more question . My second code snippet which I posted above works fine . Only reason I wasn't using it was because it was adding and removing checkKeyPress to the event handler on every key press . But as @Lakshya told, we need to bind and unbind event handler after each state update . Can you tell if I can go ahead with this code as I don't need to add any dependency list here or add event handler directly to element . Or is it not a good practice to go with this solution (refer to my second code snippet above) . – satyam Feb 15 '21 at 20:05
  • 1
    The `useEffect` ***without*** dependency will remove and re-add the event listener and callback on ***every*** render of the component, versus ***only when*** the dependency updates. While it may not be an issue with resetting the handler it's still unnecessary to do so on every render. – Drew Reese Feb 15 '21 at 20:07

2 Answers2

13

Since you always require the latest state value of input, you will have to add checkKeyPress to your useEffect's dependency list and checkKeyPress should itself have input in the wrapper useCallback deps list.

 const checkKeyPress = useCallback((e) => {
    const { key, keyCode } = e;
    console.log(key, keyCode);
    if (keyCode === 13) {
      alert(input);
    }
  },[input]);

  useEffect(() => {
    window.addEventListener("keydown", checkKeyPress);
    return () => {
      window.removeEventListener("keydown", checkKeyPress);
    };
  }, [checkKeyPress]);

In your older code ,an empty array in useEffect means that you're adding an event listener to the checkKeyPress that was available after first render. Now that checkKeyPress closes over the default value of input.

You need to ensure that every time that input updates, latest checkKeyPress (one which closes over the latest input) is added as an event handler and the old one removed.

Lakshya Thakur
  • 8,030
  • 1
  • 12
  • 39
  • Hi @Lakshya Thakur - Thanks for your reply . Yes this is a possible solution but it would add and remove checkKeyPress() to the eventListener each time some input is entered . In a big application where I have many input states like text fields, radio buttons and checkboxes and I want user to submit the form using enter key , this may cause performance issues as I will have to add several input fields in the dependency list of useCallback() which will make useEffect() run again and again . – satyam Feb 15 '21 at 19:16
  • 1
    If your event listener deals with the react state then you always will need to bind and unbind event handlers as state updates. Otherwise you will be dealing with stale state. As per my current experience, there isn't any work around for this particular use-case. Also let's consider a form. You probably won't want an event listener on window but a submit event on form element. React in the bg will also be binding/unbind new event handlers as your JSX renders again and so does form. – Lakshya Thakur Feb 15 '21 at 19:20
  • Also It will be more performant than explicitly binding events to window since react uses event delegation internally. – Lakshya Thakur Feb 15 '21 at 19:28
  • Thanks for you insights . Both solutions onSubmit() and using useCallback() are good . I'll go with onSubmit() as it is conventionally considered a good practice . Thanks for your help . – satyam Feb 15 '21 at 20:13
  • adding checkKeyPress event in useEffect save my life. Thanks for your answer. – Alex Aung Apr 01 '22 at 16:31
  • You truly saved my life. In the literal sense of it. – enzo Aug 27 '23 at 17:50
4

Issue

You've enclosed the initial input state value in the checkKeyPress callback for the "keydown" event. Each time the callback is invoked you are accessing a stale state enclosure.

Solution

Instead of trying to use a global keyDown handler just attach an event handler directly to the elements you want to a callback to respond to, which is the standard way to handle DOM interactions in React anyway. This removes the stale enclosure and makes it a standard callback.

function ShowAlertExample() {
  const [input, setInput] = useState("1");
  const handleInputChange = (e) => {
    setInput(e.target.value);
  };

  const handleShowAlert = () => {
    alert(input);
  };

  const checkKeyPress = (e) => {
    const { key, keyCode } = e;
    console.log(key, keyCode);
    if (keyCode === 13) {
      alert(input);
    }
  };

  return (
    <div>
      <header className="App-header">
        <input
          value={input}
          onChange={handleInputChange}
          onKeyDown={checkKeyPress} // <-- attach event handler
        />
        <button onClick={handleShowAlert}>Show Alert</button>
      </header>
    </div>
  );
}

Edit react-keypress-event-taking-only-initial-state-values-and-not-updated-values

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 1
    Just for clarity sake, here also on each state change, a new checkKeyPress is being bound and previous one unbound for the keydown event. It's just that now React is handling that for us and already React uses event delegation for better performance. – Lakshya Thakur Feb 15 '21 at 19:35
  • @DrewReese - Thanks for the solution . Only doubt I have is if user clicks outside the input box and then presses 'Enter' key , then it may not work . However, I agree with you and Lakshya that best solution is to use onSubmit() in this case . Actually I wasn't using
    element here but was creating input fields inside
    , so faced this issue . I think I should use that . Thanks .
    – satyam Feb 15 '21 at 20:10
  • 1
    @satyam Yes, this would only work while the input has focus, but also why I asked more about your use case. If you need a global keypress handler then listening on the window/document is the way to go, and you would just need to optimize on accessing the field values. – Drew Reese Feb 15 '21 at 20:28