React.useCallback not updating with dependency

Viewed 8129

I'm using the following component to submit comments in my app:

const App = () => {
    const [text, setText] = React.useState("");

    const send = React.useCallback(() => {
        setText("");
        console.log("sending", text);
    }, [text]);

    React.useEffect(() => {
        const handler = e => {
            switch (e.keyCode) {
                case 13: // enter
                    if (e.shiftKey) {
                        e.preventDefault();
                        send();
                    }
                    break;
            }
        }
        
        document.addEventListener("keydown", handler);
        return () => document.removeEventListener("keydown", handler);
    }, []);

    return <div className="App">
        <textarea
            className="App__text"
            value={text}
            onChange={e => {
                setText(e.target.value);
            }} />
        <button className="App__send" onClick={send}>send</button>
    </div>;
};

working demo here

It's a simple text field and button. When either the button, or shift-enter are pressed, the text in the text field is sent to the server (here we just console.log it).

The button works fine - enter "hello world" (or whatever) press the button, and the console will say hello world.

Shift-enter, however, always prints an empty string.

I'm guessing I'm misunderstanding useCallback. As I understand it, useCallback wraps your function. When one of the dependencies change, React swaps out your function without changing the wrapper function so it's still an up-to-date reference wherever it's used. Given that the send being called in useEffect appears to have the initial value of text in scope, but the one used in the onClick of the button has the newest, my assumptions seem incorrect.

I've also tried adding text as a dependency of the useEffect but

  • I don't want to remove/create/add the listener on every keystroke and
  • It's still missing the last character pressed anyway.

How can I keep a current version of my send function inside of another callback?

2 Answers

You are missing send in your dependencies, take a look at this updated code:

React.useEffect(() => {
  const handler = e => {
    switch (e.keyCode) {
      case 13: // enter
        if (e.shiftKey) {
          e.preventDefault();
          send(); // This will always be called an older version, instead of updated one
        }
        break;
    }
  }

  document.addEventListener("keydown", handler);
  return () => document.removeEventListener("keydown", handler);
}, [send]); // You need to put send here, since it is part of this component, and needs to be updated

The reason It works with it, is because send function is memoised as well:

const send = React.useCallback(() => {
  setText("");
  console.log("sending", text);
}, [text]);

so you need to make sure to update to its newer version, with its newer text (which never happened, thats why you got no text in your SHIFT+ENTER)

EDIT: Upon further investigation, it seems to me that the biggest problem was out-of sync text and listener handler.

I have modified the code to work, by removing the listener alltogether, and using onKeyDown prop directly on textarea. Take a look at this working codepen:

https://codepen.io/antonioerda/pen/zYqYWgx

Two things to make sure you do when using the useCallback hook:

  1. make sure to add the list of all the needed dependencies that are external to the method are listed.
  2. if inside this method with the usecallback you call another method that you created using the usecallback, include the dependencies there in this one as well.
Related