React.useCallback not updating with dependency - reactjs

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?

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:
make sure to add the list of all the needed dependencies that are external to the method are listed.
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

react location.replace error when triggered by keydown

React
I'm developing a popup and I want to close it down with both a button and the esc keydown.
However, even if I use the same function for both, some reason the window.location.replace() part inside the keydown function just doesn't seem to work.
everything else(console log / removing cookies) work inside the function, just not the window.location.replace('url')
I really need to use only the replace function to turn the popup off(directing into a new url with no going back), why would this happen?
Is there some things to look out for keydown I don't know about?
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}, [])
const temp = () => {
Cookies.remove('rurl', { domain: '.domain.io' })
Cookies.remove('rurl')
console.log(window.location.pathname)//for testing
window.location.replace(window.location.pathname)
console.log(2)//this is for testing. this gets printed but not the replace
}
const handleKeyDown = async (e) => {
if (e.keyCode !== 27) {
return
}
temp()
}
Got no ideas.. I tried searching for things to look out while useing keydown, but none were found
Even though I tried changing replace to href it wouln't work either.(I need to use replace though)

How do you deal with outside data in a React event listener?

In an event handler in React, how do you get and set outside data?
Here's an example that counts keyup events. Because the function is only created once, the listener always sees the keyup count as what it was initially, and it will never count above 1.
Working JSFiddle, or see code sample below.
function CountKeypresses() {
const [keypressCount, setKeypressCount] = React.useState(0);
const handleKeyup = (event) => {
setKeypressCount(keypressCount + 1); //keypressCount is always 0 here no matter what
}
//Only create the event listener once
React.useEffect(() => {
window.addEventListener("keydown", handleKeyup);
}, []);
return keypressCount;
}
ReactDOM.render(<CountKeypresses />, document.querySelector("#app"))
Use the callback form of the setter so that you can set the new value to the current previous value plus 1:
setKeypressCount(keypressCount => keypressCount + 1);
Another option is to add and remove the listener every time keypressCount changes, though it's a bit uglier:
React.useEffect(() => {
window.addEventListener("keydown", handleKeyup);
return () => window.removeEventListener("keydown", handleKeyup);
}, [keypressCount]);
If you're in a similar situation where you don't want to set state, but you need to get the current value in state, and the useEffect approach above isn't suitable, you can also store the value in a ref instead, but such situations are somewhat unusual in my experience.

react-hooks/exhaustive-deps following rule causes bug. Not all dependencies should be watched?

I've got a search component that can be triggered two ways. One by a user typing (debounced), and one by a button to retry. react-hooks/exhaustive-deps's suggestion actually causes a bug for me, as watching searchTerm fires both effects, when I only want the first to fire. Is there another approach that I should take?
export default function Search(props) {
const [searchTerm, setSearchTerm] = useState('')
const [retryCounter, setRetryCounter] = useRetryCounter() // number (int) of manual retries
function search() { ... }
const debouncedSearch = useRef(lodash.debounce(search, 300, { maxWait: 500 })).current
useEffect(() => {
debouncedSearch(searchTerm)
}, [searchTerm, debouncedSearch])
// when `searchTerm` changes, a user has typed into an <input />, so do a debounced search
useEffect(() => {
// i want to continue using the `searchTerm` provided by the user preivously
search(searchTerm)
}, [retryCounter])
// if i add `searchTerm` as a dep, then BOTH useEffects fire, and `search` gets fired twice.
// therefore, i explicitly want to ignore changes to `searchTerm` here
...
return (
<>
<input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
<button onClick={setRetryCounter}>retry</button>
</>
)
}
I also noticed on the official discussion of this rule that #StevenMyers32's question was not addressed (he has a codesandbox example), and his example seems to be a similar problem of "if i include this dependency, useEffect will incorrectly fire".

useEffect called many times or doesn't access updated variable. How to set the correct dependency on useEffect?

I'm trying to add some behaviour on keydown event, but the useEffect is not working correctly.
Here is the code:
const Test = ({}) => {
const [value, setValue] = React.useState(0);
const add= (sum) => {
setValue(value+sum);
}
React.useEffect(()=> {
const handleKeyDown = (e: KeyboardEvent) => {
console.log("keypress", value);
if (e.key === 'ArrowLeft') {
add(-1);
} else if (e.key === 'ArrowRight') {
add(1);
}
}
document.addEventListener('keydown', event => handleKeyDown(event));
return () => {
console.log("remove event")
document.removeEventListener('keydown', event => handleKeyDown(event));
};
}, [add])
return <span>Count {value}</span>
}
Here is jsfiddle code - You can see it calling many times on keypress on the console.
If I put an empty array on useEffect dependencies, when calling add function, the value will always be 0. If I put add or value on dependency array, every time I press the key useEffect will trigger many times. Is there a solution for this?
By passing an inline arrow function to addEventListener you're creating a new handler each time, and removing it doesn't work because, again, you're declaring a new function to remove--a function that was never added in the first place:
// creates a new handler function every time
document.addEventListener('keydown', event => handleKeyDown(event));
// removing a function that isn't currently registered as a
// listener on the document, because it's a brand new inline function.
document.removeEventListener('keydown', event => handleKeyDown(event));
Another instance of your keydown listener gets added every time this renders and they never get removed, so as you interact with the app they pile up.
There's no need to create a new function just to call the existing one. Try this instead:
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
Similar problem with your add function. Declaring add as an effect dependency causes the effect to run every time, because add gets re-declared on every render. Wrap it in a useCallback to prevent it from getting recreated every time:
const add = React.useCallback((sum) => {
setValue(value+sum);
}, [value]);

How to add to object using react hook useState?

Please take a look at this code snippet.
function App() {
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
}, []);
const onKeyDown = e => {
setKeyMap({ ...keyMap, [e.keyCode]: true });
};
const [keyMap, setKeyMap] = useState({});
// JSON.stringify gives only one key-value pair
return (
<div className="App">
<h1>Hello {JSON.stringify(keyMap)}</h1>
</div>
);
}
So using useEffect, I am adding an eventlistener to keydown event only once. This is similar to componentDidMount apparently since this occurs only once.
In the event handler for keydown event, I am trying to add all keycodes which were pressed to be true.
So say I press 'a', then my keyMap should look like this:
{
"65": true
}
Now if I press 'b', then my keyMap should look like this:
{
"65": true,
"66": true
}
But what's happening is this:
{
"66": true
}
Why am I unable to mutate the previous state and add to it? Like I previously had "65" as the key, but when I press another key on my keyboard, suddenly this "65" key-value disappears. That is at any given point, only one key-value pair exists for some reason. I want to be able to add to this object(keyMap object I mean) as many times as I want. Is the ... operator(spread) not working?
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
}, []);
Due to the empty array as the second parameter, this gets run only once, when the component first renders. The onKeyDown function has a reference to the state at that time, so the keyMap in setKeyMap({ ...keyMap, [e.keyCode]: true }); refers to an empty object. Any time this key handler is called, it sets the state to an empty object, plus the latest key.
Instead, you want it to use the latest state. You have two main options for how to do this
1) Don't skip the effect. This way you'll always have an event listener with the latest state. Remember to return a teardown function so that you don't have an event listener leak:
useEffect(() => {
const onKeyDown = e => {
setKeyMap({ ...keyMap, [e.keyCode]: true });
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}); // You could also pass [keyMap] as the second param to skip irrelevant changes
2) Or, use the function version of setKeyMap. It will pass you the latest value of the state.
const onKeyDown = e => {
setKeyMap(prevKeyMap => ({ ...prevKeyMap, [e.keyCode]: true }));
};
You just need to move your onKeyDown function to inside the useEffect hook like this. Note I have added keyMap in the dependency array for the useEffect hook:
https://codesandbox.io/s/red-sun-23bmu?fontsize=14

Resources