React/Redux, useEffect/useSelector, stale closures and event listeners
Answer a question
I have a React/Redux class component which I'm converting to a functional component.
Previously, it had a componentDidMount callback which added an event listener for a custom event dispatched from elsewhere in the app. This is being replaced by useEffect. The event listener, on triggering, calls a method elsewhere in this component.
This method performs actions which depend on the values retrieved from a selector. Initially I was passing useEffect an empty array of dependencies so it would only add the event listener on mount. However, obviously this results in a stale closure around the selector values. A working solution is to pass it the selector as a dependency – however, this results in the listener being removed/re-added every time the value of the selector changes, which isn't great.
I'm trying to think of a solution which only adds the event listener the one time while allowing the called method to access the current value of the selector.
Example code:
const currentValue = useSelector(state => getValue(state));
useEffect(() => {
document.addEventListener('my.custom.event', handleEvent);
return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);
const handleEvent = () => {
console.log(currentValue)
}
As is, this creates a stale closure around currentValue so that on event trigger, the value logged isn't necessarily the most recent.
Changing [] to [currentValue] in useEffect results in the expected behavior, but removes/re-adds the event listener on every change of currentValue.
Since this isn't a state value of the component, there's no option to use a callback like console.log(currentValue => console.log(currentValue)) to access the most recent value. I also played around with using useRef to hold onto the value, but I believe I'd need some way to update the ref value every time the selector's value changes, which isn't much of a solution.
In the actual component, the value of currentValue is modified in Redux by other components so changing it into a state value isn't really workable either.
I'm wondering:
- whether there's a solution to refreshing the value of the selector in the called method;
- whether the only solution is to remove/re-add the listener on dependency change, or;
- whether it's best to just leave this component as a class component and bypass this issue entirely (
componentDidMountdoesn't suffer from the stale closure issue.)
Answers
The useRef method is usually the solution for this problem, but you'll need another useEffect to update the ref with the currentValue:
const currentValue = useSelector(state => getValue(state));
const valueRef = useRef();
useEffect(() => { valueRef.current = currentValue; }, [currentValue]);
useEffect(() => {
const handleEvent = () => {
console.log(valueRef.current)
}
document.addEventListener('my.custom.event', handleEvent);
return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);
However, you can also extract the entire function out of the useEffect, and use it in a hook, so you can easily create a custom hook for event handling:
const useEventHandler = (eventName, eventHandler, eventTarget = document) => {
const eventHandlerRef = useRef();
useEffect(() => {
eventHandlerRef.current = eventHandler;
});
useEffect(() => {
const handleEvent = (...args) => eventHandlerRef?.current(...args);
eventTarget.addEventListener(eventName, handleEvent);
return (() => eventTarget.removeEventListener(eventName, handleEvent));
}, []);
}
Using the hook:
const currentValue = useSelector(state => getValue(state));
useEventHandler('my.custom.event', () => console.log(currentValue))
更多推荐
所有评论(0)