How to use useEffect/state/variables properly without user interaction? - reactjs

My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect hook to increment food. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.
I have two questions:
Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
Why does the displayed food count still equal zero? Is it because game inside <span>Food: {game.food}</span> still refers to the initial instance? If so, how do I get the latest instance?
Component:
import React from "react";
class Game {
food = 0;
}
export default function App() {
const [game, setGame] = React.useState(new Game());
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return game;
});
});
return <span>Food: {game.food}</span>;
}

Don't Mutate State Objects
React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.
For Example:
// DON'T
setGame((game) => {
// mutate and return same object
game.food += 1;
return game;
});
// DO
setGame((current) => {
// create new object with updated food value
return {
...current,
food: current.food + 1
};
});
Using the same reference will cause components to not update as expected.
useEffect Dependency Array
A useEffect without a dependency array will trigger every time the component renders.
If you wish for the useEffect to only trigger on mount provide an empty dependency array.
For Example:
// Every Render
useEffect(() => {
alert('I trigger every render');
});
// On Mount
useEffect(() => {
alert('I trigger on mount');
}, []);
// Everytime the reference for game changes
useEffect(() => {
alert('I trigger everytime the game state is update');
}, [game]);

Conclusion
"Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).
Solution
const initialGame = {
food: 0
}
export default function App() {
const [game, setGame] = React.useState(initialGame);
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return {...game};
});
}, []);
return <span>Food: {game.food}</span>;
}

No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st while loop, 2nd requestAnimationFrame and 3rd setInterval. while loop is discouraged because it will block event loop and canceling/stopping can be tedious.
double mount ? i think its react double checking function, which does this only dev mode. Once you switch to requestAnimationFrame you won't be having that issue.
use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.
updating states
useEffect(() => {
setGame((current) => {
const newState = { ...current, food: current.food + 1 }
return newState
})
}, [])
using setInterval to act as loop
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000)
return () => clearInterval(id)
}, [])
using requestAnimationFrame to act as loop
// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()
const animate = (time) => {
setCount((count) => count + 1)
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once

Related

React useEffect to have different behavior on first load and subsequent updates

I am using React with typescript and I want to convert my class component to a functional component, but my class component has two different componentDidMount and comonentDidUpdate behaviors:
componentDidMount() {
this.props.turnResetOff();
}
componentDidUpdate() {
if (this.props.resetForm) {
this.resetChangeForm();
this.props.turnResetOff();
}
}
I just want my form to reset every time it loads except the first time, because I have a menu drop-down that allows clearing the form but I want the data to not reset on mount.
I tried using this: componentDidMount equivalent on a React function/Hooks component?
const turnResetOff = props.turnResetOff;// a function
const resetForm = props.resetForm;
const setPersonId = props.setPersonId;// a function
useEffect(() => {
turnResetOff();
}, [turnResetOff]);
useEffect(() => {
const resetChangeForm = () => {/*definition*/};
if (resetForm) {
resetChangeForm();
turnResetOff();
}
}, [resetForm, turnResetOff, setPersonId]);
However, this causes an infinite re-render. Even if I useCallback for turnResetOff:
turnResetOff={useCallback(() => {
if (shouldReset) {
setShouldReset(false);
}
}, [shouldReset])}
I also tried using useRef to count the number of times this has been rendered, with the same result (infinite rerender - this is a simplified version even).
const [shouldReset, setShouldReset] = useState<boolean>(false);
const mountedTrackerRef = useRef(false);
useEffect(() => {
if (mountedTrackerRef.current === false) {
console.log("mounted now!");
mountedTrackerRef.current = true;
// props.turnResetOff();
setShouldReset(false);
} else {
console.log("mounted already... updating");
// if (props.resetForm) {
if (shouldReset) {
// resetChangeForm();
// props.turnResetOff();
setShouldReset(false);
}
}
}, [mountedTrackerRef, shouldReset]);
When you call useEffect() you can return a clean-up function. That cleanup function gets called when the component is unmounted. So, perhaps what you want to do is this: when you are called with turnResetOff then call it, and return a function that calls turnResetOff. The return function will be called when the component unmounts, so next time the component mounts it won't reset.
Something to along these lines:
useEffect(
() => {
turnResetOff()
return () => {setShouldReset(false)}
}
,[turnResetOff, setShouldReset])
Using the logic you have in the class component, the fellowing should give you identical behavior in a functional component
const turnResetOff = props.turnResetOff;// a function
const resetForm = props.resetForm;
const setPersonId = props.setPersonId;// a function
// useEffect with an empty dependency array is identical to ComponentDidMount event
useEffect(() => {
turnResetOff();
}, []);
// Just need resetForm as dependency since only resetForm was checked in the componentDidUpdate
useEffect(() => {
const resetChangeForm = () => {/*definition*/};
if (resetForm) {
resetChangeForm();
turnResetOff();
}
}, [resetForm]);

Updated state unavailable when accessing inside a method getting called from useEffect [React]

In a functional React component, I'm trying to do a fairly simple operation-
const TestClass = ({lastIndex}) => {
const [activeIndex, setActiveIndex] = useState(0);
const [fullScreenGallery, setFullScreenGallery] = useState(false);
const handleKeyChange = (e) => {
switch(e.code) {
case 'ArrowLeft':
if(activeIndex - 1 < 0) return setActiveIndex(lastIndex);
return setActiveIndex(activeIndex-1);
case 'ArrowRight':
if(activeIndex + 1 > lastIndex) return setActiveIndex(0);
return setActiveIndex(activeIndex+1);
case 'Escape':
if(fullScreenGallery) {
// closeFullScreenGallery();
return;
}
}
}
useEffect(() => {
document.addEventListener('keydown', handleKeyChange);
// other event listeners
}, []);
return (
<div>
// some code
</div>
)
}
Issue is, inside handleKeyChange, the value of activeSlide doesn't change after 1, even when I press the key. Can anyone point-out what I'm doing wrong? Same goes for fullScreenGallery.
If I try to add activeSlide and fullScreenGallery as dependencies in useEffect, my event listener starts getting adding whenever the dependency changes and I end up crashing my page.
This code was part of a class component and I'm trying to convert this class to a functional component. In the class component, useEffect code was written inside componentDidMount.
You need to "watch" for activeIndex changes.
by setting [] as you did, your useEffect is only launched once. Therefore your activeIndex varaible which is used inside this function always contains the initial value.
Learn more here.
https://fr.reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
useEffect(() => {
document.addEventListener('keydown', handleKeyChange);
// other event listeners
}, [activeIndex]);
you also need to removelisteners now. or you will rapidly become overwhelmed by the increasing number of events.
useEffect(() => {
document.addEventListener('keydown', handleKeyChange);
return () => {
document.removeEventListener('keydown', handleKeyChange);
}
// other event listeners
}, [activeIndex]);
I'm still not sure if hooks are better than classes.

does setInterval inside a useEffect can recognize an updated state?

I'm working on an app which requires a user location every X seconds. When the component is mounted, the interval starts and fetch the GPS location and compare it to the old one which is saved on the component state.
The thing is, the interval is comparing only to the default state of the variable, so if the new value is different than the old one, a setter is called to change that value.
I used useEffect on the location var so whenever it changes, to just print it and it does correctly by the new value.
but the interval keeps using the default value given in useState hook.
What is it that I'm missing here.
An example of my code is below:
const [currentLocation, setCurrentLocation] = useState(null);
let locationInterval = null;
useEffect(() => {
locationInterval = setInterval(async () => {
console.log("IN INTERVAL");
navigator.geolocation.getCurrentPosition((location, error) => {
if (location) {
location = [location.coords.latitude, location.coords.longitude];
/// currentLocation is not updating here for some reason
if (JSON.stringify(location) !== JSON.stringify(currentLocation)) {
alert(
`the new location in interval ${location} old location: ${currentLocation}`
);
setCurrentLocation(location);
}
} else {
console.log(error);
}
});
}, 15000);
}, [map]);
useEffect(() => {
return () => {
clearInterval(locationInterval);
};
}, []);
useEffect(() => {
/// currentLocation is updated here from the setInterval
console.log("newlocation", currentLocation);
}, [currentLocation]);
The reason is because currentLocation value is not given as a dependency in the useEffect, therefore that useEffect callback function only has access to the original useState value.
May I ask are there any React warning on your terminal where you run your react app?
Something like
React Hook useEffect has a missing dependency: 'currentLocation'
But on the side node, if you add currentLocation to the dependency,since because you are updating currentLocation in your useEffect and if it's updated, it will re-run the useEffect callback again.
You can not get an updated state in setInterval because callback inside useEffect hook only called when its dependency array updates([map]).
When it is called currentLocation is passed as a parameter to setInterval's callback. Which is the default value for useState at that time.
To prevent this you can do this.
const [currentLocationRef] = useRef(null);
useEffect(() => {
locationInterval = setInterval(async () => {
//....
if (JSON.stringify(location) !== JSON.stringify(currentLocationRef.current)) {
currentLocationRef.current = location;
}
}, 15000);
return () => {
clearInterval(locationInterval);
};
}, [map, currentLocationRef]);
Also, you should return clearInterval in the same useEffect Callback.
The reason behind this is currentLocationRef.current is different from currentLocation. The first one is the getter function and the second one is value. That's why the first one is still able to access updated value while the second one cannot.

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.

React: Trying to rewrite ComponentDidUpdate(prevProps) with react hook useEffect, but it fires when the app starts

I'm using a componentDidUpdate function
componentDidUpdate(prevProps){
if(prevProps.value !== this.props.users){
ipcRenderer.send('userList:store',this.props.users);
}
to this
const users = useSelector(state => state.reddit.users)
useEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
but it I get the message 'users changed' when I start the app. But the user state HAS NOT changed at all
Yep, that's how useEffect works. It runs after every render by default. If you supply an array as a second parameter, it will run on the first render, but then skip subsequent renders if the specified values have not changed. There is no built in way to skip the first render, since that's a pretty rare case.
If you need the code to have no effect on the very first render, you're going to need to do some extra work. You can use useRef to create a mutable variable, and change it to indicate once the first render is complete. For example:
const isFirstRender = useRef(true);
const users = useSelector(state => state.reddit.users);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log('users changed')
console.log({users})
}
}, [users]);
If you find yourself doing this a lot, you could create a custom hook so you can reuse it easier. Something like this:
const useUpdateEffect = (callback, dependencies) => {
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
return callback();
}
}, dependencies);
}
// to be used like:
const users = useSelector(state => state.reddit.users);
useUpdateEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.
As from: Using the Effect Hook
This, it will be invoked as the component is painted in your DOM, which is likely to be closer to componentDidMount.

Resources