Reputation: 1835
I would like to expose a concern I have regarding hooks.
While refactoring/rewriting a react library (react-numpad) using hooks for learning purposes, I have been stuck with the implementation for the eventListener on key event.
With class, this is simply accomplished by adding the addListener inside the componentDidMount and componentWillUnmount lifecycles.
useKeyPress hook
update callback
NumPad component is the parent with the state of the value
hook usage
Keypad component is the child receiving value as props and calling update on keydown as callback. This value is not updated if useEffect (inside the hook) has empty array
Now on each render addListener is added and removed. This wasn't happening before but only on when mounting or unmounting a component.
There is away to have the same behaviour with hooks as with the use of a class?
Upvotes: 1
Views: 905
Reputation: 475
You don't need to add or remove event listeners in every render, and adding them only once like before is perfectly possible, by using useEffect with an empty array as the second argument. Something like:
useEffect(() => {
// your didMount code here
return () => {
// your willUnmount code here
}
}, [])
Your problem doing this is that, as the function is only run once, the values accesible from it (such as props, or other outside-scope variables) won't get updated as they change.
As an example:
const [foo, setFoo] = setState(1)
useEffect(() => {
const id = window.setInterval(() => console.log(foo), 1000)
return () => window.clearInterval(id)
}, [])
// ...
setFoo(2)
This code will always print 1, even after calling setFoo, because the function is bound to the old value of foo.
When you only need to keep updating some state based on itself and some other value, the best option is using a reducer with useReducer. You can use dispatch from the useEffect function (or any other place), and the reducer will be called with the updated state, allowing it to read it and update it as needed. Just pass any new info on the action itself.
On your case of use, you'd just dispatch the relevant event info, and have the reducer deal with the updates.
If you can use standard react events instead of any addEventListener, you won't have to think about this, because the function will be updated on every render, without any action from your part. You can pass functions as props so they get used from a child component too.
You can use a ref instead, because they always return the same object, mutating it instead of creating a new one. This makes any function with access to the ref able to read the current value.
Example:
const fooRef = useRef(1)
fooRef.current = foo
useEffect(() => {
const id = window.setInterval(() => console.log(fooRef.current), 1000)
return () => window.clearInterval(id)
}, [])
// ...
fooRef.current = 2
You can use the ref for the values, or even use it for a function that you'd call from the event handler. This isn't the cleaner path, but you can get it to work.
Upvotes: 2