React useEffect hook with empty dependency render multiple times - reactjs

I always thought useEffect with empty dependencies will only render once. Here is my code snippet but it renders 2 times:
useEffect(() => {
let entryDataArray = [];
for (let i = 0; i < days; ++i) {
let entryData = [];
let entries = eventEntryList[i];
console.log('entries = ', entries);
for (var j = 0; j < entries.length; ++j) {
console.log('j = ', j);
console.log('entries[j] 1111111 = ', entries[j]);
if (entries[j].runGroup[i] === NOT_ATTENDING) {
continue;
}
console.log('entries[j] 2222222 = ', entries[j]);
let entry = {
lastName: entries[j].userLastName,
firstName: entries[j].userFirstName,
email: entries[j].email,
carNumber: entries[j].carNumber,
paymentMethod: entries[j].paymentMethod,
entryFee: entries[j].entryFee,
paymentStatus: entries[j].paymentStatus ? 'Paid' : 'Unpaid'
};
entryData.push(entry);
}
entryDataArray.push(entryData);
}
setEntryListArray(entryDataArray);
setShowLoading(false);
}, []);
console output shows it renders 2 times. The first time, for loop works as it supposed to - "continue" works under "if (entries[j].runGroup[i] === NOT_ATTENDING)". The 2nd time, "continue" did not get executed.
Am I missing something?

To be clear: useEffect with an empty dependency array runs every time the component mounts. This could be more than once if you have unmounted and remounted your component by accident.
First make sure you're not in StrictMode, as this would be expected in strict mode.
If you aren't in strict mode include a return function in your useEffect which will run on every unmount to detect whether or not your component is unmounting.
useEffect(() => {
// your code
return () => {
// will run on every unmount.
console.log("component is unmounting");
}
}, [])

I thought you run your app in dev mode right?. On Dev mode every time you make the change it hot reload your component every time you make the change to your code.

Related

Why is this useEffect causing an infinite loop of requests to '/analysis api on the backend

This is the backend node.js route
app.post('/analysis' , async (req , res) => {
const {temp , humidity} = req.body.data.current
let predictedCrops:any[] = []
let humidityDiffernece1
let humidityDiffernece2
let tempDifference1
let tempDifference2
//Getting all crops and optimums from databse so they are in an array (Easier to work with)
const data = await CropOptimum.find({})
data.map(({name , min_optimum_temp , max_optimum_temp , min_optimum_humidity , max_optimum_humidity }) => {
if(temp >= min_optimum_temp && temp <= max_optimum_temp && humidity >= min_optimum_humidity && humidity <= max_optimum_humidity) {
const data = {
name: name,
msg: 'Perfect Temperature and Humidity for this crop'
}
predictedCrops.push(data)
// console.log(`Perfect Match ${name}`)
} else if (temp >= min_optimum_temp && temp <= max_optimum_temp && humidity <= min_optimum_humidity) {
humidityDiffernece1 = min_optimum_humidity - humidity
// console.log(`Humidity is less than minimum and difference is ${humidityDiffernece1}`)
} else if (temp >= min_optimum_temp && temp <= max_optimum_temp && humidity >= max_optimum_humidity ) {
humidityDiffernece2 = humidity - max_optimum_humidity
// console.log(`Humidity is more than maximum and difference is ${humidityDiffernece2}`)
} else if(humidity >= min_optimum_humidity && humidity <= max_optimum_humidity && temp <= min_optimum_temp) {
tempDifference1 = min_optimum_temp - temp
// console.log(`Temperature is less than minimum , difference is ${tempDifference1}`)
} else if(humidity >= min_optimum_humidity && humidity <= max_optimum_humidity && temp >= max_optimum_temp) {
tempDifference2 = temp - max_optimum_temp
// console.log(tempDifference2)
} else {
// console.log("NO MATCHES")
}
})
console.log(predictedCrops)
res.send(predictedCrops)
})
This is where I call the API or make the request to /analysis in react frontend
useEffect( () => {
navigator.geolocation.getCurrentPosition((postion) => {
setLatitiude(postion.coords.latitude)
setLongitude(postion.coords.longitude)
})
getWeatherDetails()
}, [isAnalyzing , predictedCrop])
This is the getWeatherDetails function
const getWeatherDetails = async() => {
try {
const response = await axios.get(`${API_ENDPOINT}lat=${latitude}&lon=${longitude}&exclude=hourly,daily&units=metric&appid=${API_KEY}`)
console.log(response.data)
//Send temp and humidity to backend for analysis
const dataToSend = response.data
const sentWeatherData = await axios.post("http://localhost:4000/analysis" , {
data: dataToSend
}).then((response) => {
let dataObject = response.data;
setPredictedCrop(dataObject)
console.log(predictedCrop)
})
} catch {
console.log("error")
}
}
So basically, what is happening is that when a button is clicked it sets isAnalyzing state to true and the process starts. When I remove predictedCrop from the dependency it does not cause the loop however the predictedCrop state will not update on the line where it says setPredictedCrop(dataObject). When I add the predictedCrop to the dependency of the useEffect it causes the loop but sets the state successfully.
I am kind of a beginner at this so any help is appreciated, thank you.
When I remove predictedCrop from the dependency it does not cause the loop however the predictedCrop state will not update on the line where it says setPredictedCrop(dataObject). When I add the predictedCrop to the dependency of the useEffect it causes the loop but sets the state successfully.
You got the wrong concept here. Mentioning a state variable or a prop in a dependency array is not a necessity for the state variable or a prop value to change. It denotes that any changes to that value will trigger the useEffect.
In your case, if predictedCrop, is not in the dependency array, it's state will change, it's just that, it won't trigger the useEffect, whenever it's updated.
Putting it in the array, will trigger the useEffect and will cause infinite loop.
The reason the console.log's are empty is because react state setting is asynchronous. It's probably setting it fine, it just happens shortly after the console.log because of how React batches state updates and runs them later seamlessly.
I would suggest logging it in the render method or something.
See here for more details: https://beta.reactjs.org/apis/usestate#ive-updated-the-state-but-logging-gives-me-the-old-value.
So removing predictedCrop from deps array is fine, its just your logging is confusing you :).
Its worth noting however you probably should not be doing the network call in the useEffect anyway. It would be better to do it in the event handler of the button itself. useEffect is for side effects, and this isnt one -- its in response to button click.
See here https://beta.reactjs.org/learn/synchronizing-with-effects#not-an-effect-buying-a-product
You wouldn’t want to buy the product twice. However, this is also why you shouldn’t put this logic in an Effect. What if the user goes to another page and then presses Back? Your Effect would run again. You don’t want to buy the product when the user visits a page; you want to buy it when the user clicks the Buy button.

How to correctly modify the state from the parent component before passing it down to child component

I have three components:
1st component
--2nd component
----3rd component
I need to pass a state and its handleState down from the first component to the second one. In the second one I need to filter through the state array values, I also need to update the db and the state so I make multiple API and handleState calls in a for loop inside of useEffect(). The third one renders the state data.
Here's the second component:
export default function LearnNewApp({ deck, handleShowAppChange, handleDecksChange }) {
let words = deck.words.filter(x => x.wordGroup == "newLearning")
useEffect(() => {
let unsubscribed = false;
if (words.length < 10) {
let vacant = 10 - words.length
let newDeck = JSON.parse(JSON.stringify(deck))
let unseenWords = newDeck.words.filter(x => x.wordGroup === "newUnseen")
for (let i = 0; i < vacant && i < unseenWords.length; i++) {
let wordUnseenToLearning = unseenWords[i]
wordUnseenToLearning.wordGroup = "newLearning"
callAPI(wordUnseenToLearning.id, "newLearning")
if (!unsubscribed) handleDecksChange(wordUnseenToLearning)
}
}
return () => unsubscribed = true;
}, [])
function memorized(word) {
callAPI(word.id, "first")
let wordLearningToFirst = {
...word, wordGroup: "first"
}
handleDecksChange(wordLearningToFirst)
}
function showAgain() {
}
if (words != []) return (
<CardPage words={words} handleShowAppChange={handleShowAppChange} leftButtonFunc={memorized} rightButtonFunc={showAgain} />
)
}
The state array that's being modified is an array of decks with words. So I'm trying to narrow down the chosen deck with .filter to up to ten words so they could be shown to the user in the third component. To do that I check how many words there are with showNew attribute. If there's less than ten of them I check if there are any new words in the deck to change from neverShown to showNew.
The code causes an error probably because it takes some time to do everything in useEffect(), not to mention it runs after render.
The third component has a button that triggers a function from the second component that also updates the db and state but not in a loop.
So the main problem is that I don't know how to properly fix the deck modification and subsequent render in the second component.

setState accept function vs object

Lets say I have a state that I would like to update:
state = {
description: "",
notes: ""
}
Is there a difference if I update the state with just an object, if I do not need to use prevState?
this.setState({description: "Blue"});
vs
this.setState((prevState)=> ({description: "Blue"}));
I've created the demo to visualize the difference
/* When we doing this state gets update after the for loops runs so,
this.state.count is same for each repeat so even if we expecting to
count increase by 5 its actually increment by 1 */
updateCountByFive = () => {
for (let a = 0; a < 5; a++) {
this.setState({
count: this.state.count + 1
});
}
};
/* In this case by using callback to set the new state,
react is smart enough to pass the latest count value to the callback
event if that new change not rendered */
updateCountByFiveCallback = () => {
for (let a = 0; a < 5; a++) {
this.setState(prevState => {
return {
count: prevState.count + 1
};
});
}
};
So it's a good practice to use callback version when you need to use current state to set the next state, since it will prevent some issues as above
This is what react doc says.
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
there is a nice article here

React hook rendering an extra time

My code is causing an unexpected amount of re-renders.
function App() {
const [isOn, setIsOn] = useState(false)
const [timer, setTimer] = useState(0)
console.log('re-rendered', timer)
useEffect(() => {
let interval
if (isOn) {
interval = setInterval(() => setTimer(timer + 1), 1000)
}
return () => clearInterval(interval)
}, [isOn])
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
Note the console.log on line 4. What I expected is the following to be logged out:
re-rendered 0
re-rendered 0
re-rendered 1
The first log is for the initial render. The second log is for the re-render when the "isOn" state changes via the button click. The third log is when setInterval calls setTimer so it's re-rendered again. Here is what I actually get:
re-rendered 0
re-rendered 0
re-rendered 1
re-rendered 1
I can't figure out why there is a fourth log. Here's a link to a REPL of it:
https://codesandbox.io/s/kx393n58r7
***Just to clarify, I know the solution is to use setTimer(timer => timer + 1), but I would like to know why the code above causes a fourth render.
The function with the bulk of what happens when you call the setter returned by useState is dispatchAction within ReactFiberHooks.js (currently starting at line 1009).
The block of code that checks to see if the state has changed (and potentially skips the re-render if it has not changed) is currently surrounded by the following condition:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
My assumption on seeing this was that this condition evaluated to false after the second setTimer call. To verify this, I copied the development CDN React files and added some console logs to the dispatchAction function:
function dispatchAction(fiber, queue, action) {
!(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;
{
!(arguments.length <= 3) ? warning$1(false, "State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;
}
console.log("dispatchAction1");
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
var update = {
expirationTime: renderExpirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
var lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
flushPassiveEffects();
console.log("dispatchAction2");
var currentTime = requestCurrentTime();
var _expirationTime = computeExpirationForFiber(currentTime, fiber);
var _update2 = {
expirationTime: _expirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// Append the update to the end of the list.
var _last = queue.last;
if (_last === null) {
// This is the first update. Create a circular list.
_update2.next = _update2;
} else {
var first = _last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
_last.next = _update2;
}
queue.last = _update2;
console.log("expiration: " + fiber.expirationTime);
if (alternate) {
console.log("alternate expiration: " + alternate.expirationTime);
}
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
console.log("dispatchAction3");
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var _eagerReducer = queue.eagerReducer;
if (_eagerReducer !== null) {
var prevDispatcher = void 0;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
_update2.eagerReducer = _eagerReducer;
_update2.eagerState = _eagerState;
if (is(_eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
{
if (shouldWarnForUnbatchedSetState === true) {
warnIfNotCurrentlyBatchingInDev(fiber);
}
}
scheduleWork(fiber, _expirationTime);
}
}
and here's the console output with some additional comments for clarity:
re-rendered 0 // initial render
dispatchAction1 // setIsOn
dispatchAction2
expiration: 0
dispatchAction3
re-rendered 0
dispatchAction1 // first call to setTimer
dispatchAction2
expiration: 1073741823
alternate expiration: 0
re-rendered 1
dispatchAction1 // second call to setTimer
dispatchAction2
expiration: 0
alternate expiration: 1073741823
re-rendered 1
dispatchAction1 // third and subsequent calls to setTimer all look like this
dispatchAction2
expiration: 0
alternate expiration: 0
dispatchAction3
NoWork has a value of zero. You can see that the first log of fiber.expirationTime after setTimer has a non-zero value. In the logs from the second setTimer call, that fiber.expirationTime has been moved to alternate.expirationTime still preventing the state comparison so re-render will be unconditional. After that, both the fiber and alternate expiration times are 0 (NoWork) and then it does the state comparison and avoids a re-render.
This description of the React Fiber Architecture is a good starting point for trying to understand the purpose of expirationTime.
The most relevant portions of the source code for understanding it are:
ReactFiberExpirationTime.js
ReactFiberScheduler.js
I believe the expiration times are mainly relevant for concurrent mode which is not yet enabled by default. The expiration time indicates the point in time after which React will force a commit of the work at the earliest opportunity. Prior to that point in time, React may choose to batch updates. Some updates (such as from user interactions) have a very short (high priority) expiration, and other updates (such as from async code after a fetch completes) have a longer (low priority) expiration. The updates triggered by setTimer from within the setInterval callback would fall in the low priority category and could potentially be batched (if concurrent mode were enabled). Since there is the possibility of that work having been batched or potentially discarded, React queues a re-render unconditionally (even when the state is unchanged since the previous update) if the previous update had an expirationTime.
You can see my answer here to learn a bit more about finding your way through the React code to get to this dispatchAction function.
For others who want to do some digging of their own, here's a CodeSandbox with my modified version of React:
The react files are modified copies of these files:
https://unpkg.com/react#16/umd/react.development.js
https://unpkg.com/react-dom#16/umd/react-dom.development.js

SetInterval and clearInterval in ReactJS

This was asked upon searching here, but the solutions were not working, or I have missed something.
This is the snippet of the code
tick(rev_new_arr) {
if(rev_new_arr.length > 0){
this.setState({
sim_play_date : rev_new_arr[0]
},()=>{
rev_new_arr.shift()
})
}else{
this.stopPlay()
}
}
playMap(){
const startDate = this.state.simulation_date;
const sim_dates = this.state.sim_dates;
const index_simdate = parseInt(sim_dates.indexOf(startDate) + 1, 0);
const new_arr = sim_dates.slice(0,index_simdate);
const rev_new_arr = new_arr.reverse();
this.timerID = setInterval(()=> this.tick(rev_new_arr), 1000);
}
stopPlay(){
console.log(this.timerID);
clearInterval(this.timerID);
}
setInterval works but when adding the function stopPlay() on click event in a button, it does not stop and still calls the tick function.
EDIT: I tried logging the timerID in console..It outputs 2 ids. I think that is the reason, why it's not stopping even calling clearInterval. What must be the reason of this?
Are your methods playMap() stopPlay() and tick() bound to the class? If not, try defining them like:
stopPlay = () => {
...
}
clearInterval is referring to global context,so bind it.
stopPlay(){
console.log(this.timerID);
clearInterval(this.timerID).bind(this);
}

Resources