I am following a Udemy course on how to register events with hooks, the instructor gave the below code:
const [userText, setUserText] = useState('');
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
});
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
Now it works great but I’m not convinced that this is the right way. The reason is, if I understand correctly, on each and every re-render, events will keep registering and deregistering every time and I simply don’t think it is the right way to go about it.
So I made a slight modification to the useEffect
hooks to below
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, []);
By having an empty array as the second argument, letting the component to only run the effect once, imitating componentDidMount
. And when I try out the result, it’s weird that on every key I type, instead of appending, it’s overwritten instead.
I was expecting setUserText(${userText}${key}
); to have new typed key append to current state and set as a new state but instead, it’s forgetting the old state and rewriting with the new state.
Was it really the correct way that we should register and deregister event on every re-render?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
The best way to go about such scenarios is to see what you are doing in the event handler.
If you are simply setting state
using previous state
, it’s best to use the callback pattern and register the event listeners only on initial mount.
If you do not use the callback pattern, the listeners reference along with its lexical scope is being used by the event listener but a new function is created with updated closure on each render; hence in the handler you will not be able to access the updated state
const [userText, setUserText] = useState(""); const handleUserKeyPress = useCallback(event => { const { key, keyCode } = event; if(keyCode === 32 || (keyCode >= 65 && keyCode <= 90)){ setUserText(prevUserText => `${prevUserText}${key}`); } }, []); useEffect(() => { window.addEventListener("keydown", handleUserKeyPress); return () => { window.removeEventListener("keydown", handleUserKeyPress); }; }, [handleUserKeyPress]); return ( <div> <h1>Feel free to type!</h1> <blockquote>{userText}</blockquote> </div> );
Method 2
Issue
[…] on each and every re-render, events will keep registering and deregistering every time and I simply don’t think it is the right way to go about it.
You are right. It doesn’t make sense to restart event handling inside useEffect
on every render.
[…] empty array as the second argument, letting the component to only run the effect once […] it’s weird that on every key I type, instead of appending, it’s overwritten instead.
This is an issue with stale closure values.
Reason: Used functions inside useEffect
should be part of the dependencies. You set nothing as dependency ([]
), but still call handleUserKeyPress
, which itself reads userText
state.
Solutions
Update: React developers proposed an RFC including new useEvent
Hook (name might change) to solve this exact type of event-related problem with dependencies.
Until then, there are alternatives depending on your use case:
1. State updater function
setUserText(prev => `${prev}${key}`);
✔ least invasive approach
✖ only access to own previous state, not other state Hooks
const App = () => {
const [userText, setUserText] = useState("");
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prev => `${prev}${key}`); // use updater function here
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []); // still no dependencies
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
2. useRef
/ mutable refs
const cbRef = useRef(handleUserKeyPress); useEffect(() => { cbRef.current = handleUserKeyPress; }); // update each render useEffect(() => { const cb = e => cbRef.current(e); // then use most recent cb value window.addEventListener("keydown", cb); return () => { window.removeEventListener("keydown", cb) }; }, []);
const App = () => {
const [userText, setUserText] = useState("");
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
const cbRef = useRef(handleUserKeyPress);
useEffect(() => {
cbRef.current = handleUserKeyPress;
});
useEffect(() => {
const cb = e => cbRef.current(e);
window.addEventListener("keydown", cb);
return () => {
window.removeEventListener("keydown", cb);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
✔ can be used for callbacks/event handlers that shall not trigger re-renders via data flow
✔ no need to manage dependencies
✖ more imperative approach
✖ only recommended as last option by React docs
Take a look at these links for further info: 1 2 3
3. useReducer
– “cheat mode”
We can switch to useReducer
and have access to current state/props – with similar API to useState
.
Variant 2a: logic inside reducer function
const [userText, handleUserKeyPress] = useReducer((state, event) => { const { key, keyCode } = event; // isUpperCase is always the most recent state (no stale closure value) return `${state}${isUpperCase ? key.toUpperCase() : key}`; }, "");
const App = () => {
const [isUpperCase, setUpperCase] = useState(false);
const [userText, handleUserKeyPress] = useReducer((state, event) => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
// isUpperCase is always the most recent state (no stale closure)
return `${state}${isUpperCase ? key.toUpperCase() : key}`;
}
}, "");
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
<button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
{isUpperCase ? "Disable" : "Enable"} Upper Case
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Variant 2b: logic outside reducer function – similar to useState
updater function
const [userText, setUserText] = useReducer((state, action) => typeof action === "function" ? action(state, isUpperCase) : action, ""); // ... setUserText((prevState, isUpper) => `${prevState}${isUpper ? key.toUpperCase() : key}`);
const App = () => {
const [isUpperCase, setUpperCase] = useState(false);
const [userText, setUserText] = useReducer(
(state, action) =>
typeof action === "function" ? action(state, isUpperCase) : action,
""
);
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(
(prevState, isUpper) =>
`${prevState}${isUpper ? key.toUpperCase() : key}`
);
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
<button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
{isUpperCase ? "Disable" : "Enable"} Upper Case
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
✔ no need to manage dependencies
✔ access multiple states and props
✔ same API as useState
✔ extendable to more complex cases/reducers
✖ slightly less performance due to inline reducer (kinda neglectable)
✖ slightly increased complexity of reducer
Inappropriate solutions
useCallback
While it can be applied in various ways, useCallback
is not suitable for this particular question case.
Reason: Due to the added dependencies – userText
here -, the event listener will be re-started on every key press, in best case being not performant, or worse causing inconsistencies.
const App = () => {
const [userText, setUserText] = useState("");
const handleUserKeyPress = useCallback(
event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
},
[userText]
);
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [handleUserKeyPress]); // we rely directly on handler, indirectly on userText
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
Declare handler function inside useEffect
Declaring the event handler function directly inside useEffect
has more or less the same issues as useCallback
, latter just causes a bit more indirection of dependencies.
In other words: Instead of adding an additional layer of dependencies via useCallback
, we put the function directly inside useEffect
– but all the dependencies still need to be set, causing frequent handler changes.
In fact, if you move handleUserKeyPress
inside useEffect
, ESLint exhaustive deps rule will tell you, what exact canonical dependencies are missing (userText
), if not specified.
const App =() => {
const [userText, setUserText] = useState("");
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [userText]); // ESLint will yell here, if `userText` is missing
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Method 3
new answer:
useEffect(() => { function handlekeydownEvent(event) { const { key, keyCode } = event; if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) { setUserText(prevUserText => `${prevUserText}${key}`); } } document.addEventListener('keyup', handlekeydownEvent) return () => { document.removeEventListener('keyup', handlekeydownEvent) } }, [])
when using setUserText
, pass the function as the argument instead of the object, the prevUserText
will be always the newest state.
old answer:
try this, it works same as your original code:
useEffect(() => { function handlekeydownEvent(event) { const { key, keyCode } = event; if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) { setUserText(`${userText}${key}`); } } document.addEventListener('keyup', handlekeydownEvent) return () => { document.removeEventListener('keyup', handlekeydownEvent) } }, [userText])
because in your useEffect()
method, it depends on the userText
variable but you don’t put it inside the second argument, else the userText
will always be bound to the initial value ''
with argument []
.
you don’t need to do like this, just want to let you know why your second solution doesn’t work.
Method 4
You’ll need a way to keep track of the previous state. useState
helps you keep track of the current state only. From the docs, there is a way to access the old state, by using another hook.
const prevRef = useRef(); useEffect(() => { prevRef.current = userText; });
I’ve updated your example to use this. And it works out.
const { useState, useEffect, useRef } = React;
const App = () => {
const [userText, setUserText] = useState("");
const prevRef = useRef();
useEffect(() => {
prevRef.current = userText;
});
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${prevRef.current}${key}`);
}
};
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Method 5
For your use case, useEffect
needs a dependency array to track changes and based on the dependency it can determine whether to re-render or not. It is always advised to pass a dependency array to useEffect
. Kindly see the code below:
I have introduced useCallback
hook.
const { useCallback, useState, useEffect } = React; const [userText, setUserText] = useState(""); const handleUserKeyPress = useCallback(event => { const { key, keyCode } = event; if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) { setUserText(prevUserText => `${prevUserText}${key}`); } }, []); useEffect(() => { window.addEventListener("keydown", handleUserKeyPress); return () => { window.removeEventListener("keydown", handleUserKeyPress); }; }, [handleUserKeyPress]); return ( <div> <blockquote>{userText}</blockquote> </div> );
Method 6
The accepted answer is working but if you are registering BackHandler
event, make sure to return true
in your handleBackPress
function, e.g:
const handleBackPress= useCallback(() => { // do some action and return true or if you do not // want the user to go back, return false instead return true; }, []); useEffect(() => { BackHandler.addEventListener('hardwareBackPress', handleBackPress); return () => BackHandler.removeEventListener('hardwareBackPress', handleBackPress); }, [handleBackPress]);
Method 7
Here is the useRef solution based on @ford04’s answer and moved to custom hook. I like it most because it doesn’t require adding any manual dependencies and allows to hide all the complexity in the custom hook.
const useEvent = (eventName, eventHandler) => { const cbRef = useRef(eventHandler) useEffect(() => { cbRef.current = eventHandler }) // update after each render useEffect(() => { console.log("+++ subscribe") const cb = (e) => cbRef.current(e) // then use most recent cb value window.addEventListener(eventName, cb) return () => { console.log("--- unsubscribe") window.removeEventListener(eventName, cb) } }, [eventName]) return }
Usage in App:
function App() { const [isUpperCase, setUpperCase] = useState(false) const [userText, setUserText] = useState("") const handleUserKeyPress = (event) => { const { key, keyCode } = event if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) { const displayKey = isUpperCase ? key.toUpperCase() : key const newText = userText + displayKey setUserText(newText) } } useEvent("keydown", handleUserKeyPress) return ( <div> <h1>Feel free to type!</h1> <label> <input type="checkbox" defaultChecked={isUpperCase} onChange={() => setUpperCase(!isUpperCase)} /> Upper Case </label> <blockquote>{userText}</blockquote> </div> ) }
Method 8
In the second approach, the useEffect
is bound only once and hence the userText
never gets updated. One approach would be to maintain a local variable which gets updated along with the userText
object on every keypress.
const [userText, setUserText] = useState(''); let local_text = userText const handleUserKeyPress = event => { const { key, keyCode } = event; if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) { local_text = `${userText}${key}`; setUserText(local_text); } }; useEffect(() => { window.addEventListener('keydown', handleUserKeyPress); return () => { window.removeEventListener('keydown', handleUserKeyPress); }; }, []); return ( <div> <h1>Feel free to type!</h1> <blockquote>{userText}</blockquote> </div> );
Personally I don’t like the solution, feels anti-react
and I think the first method is good enough and is designed to be used that way.
Method 9
you dont have access to the changed useText state. you can comapre it to the prevState. store the state in a variable e.g.: state like so:
const App = () => { const [userText, setUserText] = useState(''); useEffect(() => { let state = '' const handleUserKeyPress = event => { const { key, keyCode } = event; if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) { state += `${key}` setUserText(state); } }; window.addEventListener('keydown', handleUserKeyPress); return () => { window.removeEventListener('keydown', handleUserKeyPress); }; }, []); return ( <div> <h1>Feel free to type!</h1> <blockquote>{userText}</blockquote> </div> ); };
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0