Access state from Event Handler created within React.js UseEffect Hook - reactjs

In my component I'm setting up an event listener within the useEffect hook:
useEffect(() => {
const target = subtitleEl.current;
target.addEventListener("click", () => {
console.log("click");
onSubtitleClick();
});
return () => {
target.removeEventListener("click", null);
};
}, []);
.. but when I call onSubtitleClick, my state is stale - it's the original value. Example code here. How can I access state from the event handler that was setup with the useEffect?

Your event listener registers a function (reference) which has count as 0 in the environment it is defined and when a new render happens, your reference is not being updated which is registered with that element and that registered function reference still knows that count is 0 even though count has been changed but that updated function was not registered which knows the updated value in its context. So you need to update event listener with new function reference.
useEffect(() => {
const target = subtitleEl.current;
target.addEventListener("click", onSubtitleClick);
return () => {
console.log("removeEventListener");
target.removeEventListener("click", onSubtitleClick);
};
}, [onSubtitleClick]);
However, you don't need that messy code to achieve what you are doing now or similar stuff. You can simply call that passed function on click and don't attach to element through ref but directly in jsx.
<div
className="panelText"
style={{ fontSize: "13px" }}
onClick={onSubtitleClick}
ref={subtitleEl}
>
Button2
</div>

I think basically the addeventlistener creates a closure with its own version of count separate to the parent component's. A simple fix is to allow the useEffect function to rerun on changes (remove the [] arg):
useEffect(() => {
const target = subtitleEl.current;
target.addEventListener("click", () => {
console.log("click");
onSubtitleClick();
});
return () => {
target.removeEventListener("click", null);
};
}); // removed [] arg which prevents useeffect being run twice

Related

Context value not updating inside event listener in React

I am building a simple game in react. The problem is context is properly updated but the context value inside an event listener function is not updating. If I access the value outside the event function then the new updated value is rendered.
For the first keyup event the value will be 0 but for the next events, it should be a new updated value.
const updateGame = useUpdateGame();
const gameData = useGameData();
//Assume Default gameData.board value is 0
// Assume context value updated to 5
useEffect(() => {
document.addEventListener("keyup", (e) => {
e.stopImmediatePropagation();
console.log(gameData.board) //0
handleKeyPress(e.key, gameData.board, updateGame);
});
}, []);
console.log(gameData.board) //5
The event listener has closed over the old value of context (think closures). You will need to keep the updated value of the context in the event listener.
This could either be done by defining the event listener everytime the context value changes or using a ref.
Register and clear the event listener in the useEffect after every render when gameData changes.
const updateGame = useUpdateGame();
const gameData = useGameData();
useEffect(() => {
let listener = (e) => {
e.stopImmediatePropagation();
handleKeyPress(e.key, gameData.board, updateGame);
};
document.addEventListener("keyup", listener);
return () => {
document.removeEventListener("keyup", listener);
};
}, [gameData]); //updateGame should be added here too ideally
Keep a ref which mimics the value you are looking for. ref are stable containers so they will always have the correct value of the state
const updateGame = useUpdateGame();
const gameData = useGameData();
const gameDataRef = useRef(gameData?.board ?? null);
useEffect(() => {
document.addEventListener("keyup", (e) => {
e.stopImmediatePropagation();
handleKeyPress(e.key, gameDataRef.current, updateGame);
});
}, []);
useEffect(() =>{
gameDataRef.current = gameData.board;
},[gameData]);
Whenever gameData changes, you should add another keyup listener (and remove the one that currently exists, if any) so that the state value in the closure is the most up-to-date one - right now, the empty dependency array means that the gameData in the handler always refers to its initial value, which isn't what you want.
updateGame is also referenced inside the listener, so it should be in the dependency array as well.
useEffect(() => {
const handler = (e) => {
e.stopImmediatePropagation();
handleKeyPress(e.key, gameData.board, updateGame);
};
document.addEventListener("keyup", handler);
return () => document.removeEventListener("keyup", handler);
}, [gameData, updateGame]);
Consider enabling and following the exhaustive-deps rule to automatically prompt you to fix these sorts of potential mistakes in the future.

How to update the unmount handler in a useEffect without firing it repeatedly?

Another developer came to me with a tricky question today. She proposed the following:
A.) She wants to fire an event whenever a modal closes.
B.) She does not want the event to fire at any other time.
C.) The event must be up to date with state of the component.
A basic example is like so:
const ModalComponent = () => {
const [ eventData, setEventData ] = useState({ /* default data */ });
useEffect(() => {
return () => {
// happens anytime dependency array changes
eventFire(eventdata);
};
}, [eventData]);
return (
<>
// component details omitted, setEventData used in here
</>
);
};
The intention is that when the modal is closed, the event fires. However, user interactions with the modal cause state updates that change the value of eventData. This leads to the core of the problem:
Leaving eventData out of the useEffect dependency array causes it to fire only at time of modal closing, but the value is stale. It's the value that it was at the time the component mounted.
Placing eventData in the useEffect dependency array causes the event to fire over and over, whenever the data changes and the component re-renders and updates useEffect.
What is the solution to this? How can you access up-to-date data yet only act on it at time of unmounting?
Store eventData in a ref.
const [eventData, setEventData] = useState({ /* default data */ });
const ref = useRef();
ref.current = eventData;
useEffect(() => () => eventFire(ref.current), []);
This will keep the value always up to date since it won't be locked into the function closure and will remove the need for triggering the effect every time eventData changes.
Also, you can extract this logic into a custom hook.
function useStateMonitor(state) {
const ref = useRef();
ref.current = state;
return () => ref.current;
}
And usage would be like this
const [eventData, setEventData] = useState({ /* default data */ });
const eventDataMonitor = useStateMonitor(eventData);
useEffect(() => () => eventFire(eventDataMonitor()), []);
Here's an working example

How do I avoid trigger useEffect when I set the same state value?

I use react hook call an API and set the data to state and I still have some control view can change the value, but when I change it, my API function trigger again, it cause my view re render multiple times.
How do I use my fetchData function just like in class component componentDidMount function ?
const [brightness, setBrightness] = useState(0);
useEffect(() => {
fetchData();
});
const fetchData = async () => {
const value = await something(); // call API get the first value
setBrightness(value);
};
return (
<View>
<SomeView value={brightness} onComplete={(value) => setBrightness(value)}
</View>
);
Your useEffect will be triggered on every render because you haven't provided the 2nd argument.
The 2nd argument is what the effect is conditional upon. In your case you only want it to run once so you can provide an empty array.
useEffect(() => {
// Run once
}, [])
Lets say you wanted it to fetch anytime some prop changed, you could write
useEffect(() => {
// run every time props.myValue changes
}, [props.myValue])
See https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

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.

how to use the useEffect hook on component unmount to conditionally run code

For some odd reason the value of props in my "unmount" useEffect hook is always at the original state (true), I can console and see in the devtools that it has changed to false but when the useEffect is called on unmount it is always true.
I have tried adding the props to the dependancies but then it is no longer called only on unmount and does not serve it's purpose.
Edit: I am aware the dependancy array is empty, I cannot have it triggered on each change, it needs to be triggered ONLY on unmount with the update values from the props. Is this possible?
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, []);
How can I conditionally run my code on unmount with the condition being dependant on the updated props state?
If you want code to run on unmount only, you need to use the empty dependency array. If you also require data from the closure that may change in between when the component first rendered and when it last rendered, you'll need to use a ref to make that data available when the unmount happens. For example:
const onUnmount = React.useRef();
onUnmount.current = () => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
}
React.useEffect(() => {
return () => onUnmount.current();
}, []);
If you do this often, you may want to extract it into a custom hook:
export const useUnmount = (fn): => {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffect(() => () => fnRef.current(), []);
};
// used like:
useUnmount(() => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
});
The dependency list of your effect is empty which means that react will only create the closure over your outer variables once on mount and the function will only see the values as they have been on mount. To re-create the closure when report.data.draft changes you have to add it to the dependency list:
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, [report.data.draft]);
There also is an eslint plugin that warns you about missing dependencies: https://www.npmjs.com/package/eslint-plugin-react-hooks
Using custom js events you can emulate unmounting a componentWillUnmount even when having dependency. Here is how I did it.
Problem:
useEffect(() => {
//Dependent Code
return () => {
// Desired to perform action on unmount only 'componentWillUnmount'
// But it does not
if(somethingChanged){
// Perform an Action only if something changed
}
}
},[somethingChanged]);
Solution:
// Rewrite this code to arrange emulate this behaviour
// Decoupling using events
useEffect( () => {
return () => {
// Executed only when component unmounts,
let e = new Event("componentUnmount");
document.dispatchEvent(e);
}
}, []);
useEffect( () => {
function doOnUnmount(){
if(somethingChanged){
// Perform an Action only if something changed
}
}
document.addEventListener("componentUnmount",doOnUnmount);
return () => {
// This is done whenever value of somethingChanged changes
document.removeEventListener("componentUnmount",doOnUnmount);
}
}, [somethingChanged])
Caveats: useEffects have to be in order, useEffect with no dependency have to be written before, this is to avoid the event being called after its removed.

Resources