Why useEffect doesn't call callback function if state doesn't change? - javascript

I have the following code(react 16.8.6, react-dom: 16.8.6):
import React, { useState, useEffect, } from 'react';
import ReactDOM from 'react-dom';
function App(props) {
const [counter, setCounter] = useState(0);
console.log(1)
useEffect(() => {
console.log(2)
setTimeout(() => {
console.log(3)
setCounter(1);
}, 10000)
});
return counter;
}
ReactDOM.render(<App />, document.getElementById('root'));
When App component is rendered first time, it prints:
1
2
After 10 seconds it prints:
3, 1, 2
Everything that was before is understandable for me, but now after 10 seconds it prints
3, 1
i.e. function that is passed to useEffect isn't called. I assume it's related to state somehow (as it doesn't change, if it changes useEffect works fine). Could you explain this behaviour?

According to the docs
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
An update never occurred after you call setCounter a second time, because 1 === 1 is always the same.
If you actually increment your counter by one every time you'll get your desired effect.
function App(props) {
const [counter, setCounter] = useState(0);
console.log(1)
useEffect(() => {
console.log(2)
setTimeout(() => {
console.log(3)
setCounter(counter + 1);
}, 10000)
});
return counter;
}
Live example

From the React docs:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.
Thats exactly what happens here: The component rerenders, but as the current state is 1 and you set it to 1 after the second timeout React "bails out" and thus does not retrigger the effect.
Sidenote: The effect should really only depend on counter, not on an update in general.

Every time your component renders, useEffect will be called because you do not provide an array as a second parameter.
If you read the docs, you will read, that if you do not provide a second parameter, it will be called every time.
If you provide an empty array, it will only be called on the first render.
If you provide a variable into the array, it will only be executed if that variable changes.
3,1 will be called because after the 2 is called, the timeout is set again and will print 3. After updating the state with setCounter(1);, the component rerenders and 1 will be called again.
Hope this helps. Happy coding.

Related

What is the correct way to memoize this useEffect dependency?

I use useEffect to listen for a change in location.pathname to auto-scroll back to the top of the page (router) when the route changes. As I have a page transition (that takes pageTransitionTime * 1000 seconds), I use a timer that waits for the page transition animation to occur before the reset takes place. However, on the first load/mount of the router (after a loading page), I do NOT want to wait for the animation as there is no page animation.
Observe the code below, which works exactly as intended:
useEffect(() => {
const timer = setTimeout(() => {
window.scrollTo(0,0)
}, firstVisit.app ? 0 : pageTransitionTime * 1000 )
return () => clearTimeout(timer)
}, [location.pathname, pageTransitionTime])
The error I face here is that firstVisit.app isn't in the dependency array. I get this error on Terminal:
React Hook useEffect has a missing dependency: 'firstVisit.app'. Either include it or remove the dependency array react-hooks/exhaustive-deps
firstVisit.app is a global redux variable that is updated in the same React component by another useEffect, setting it to false as soon as the router is mounted (this useEffect has no dependency array, so it achieves it purpose instantly).
// UPON FIRST MOUNT, SET firstVisit.app TO FALSE
useEffect(() => {
if (firstVisit.app) {
dispatch(setFirstVisit({
...firstVisit,
app: false
}))
}
})
The problem is, when I include firstVisit.app in the dependency array in the first useEffect, the page will auto-reset scroll to (0,0) after pageTransitionTime, affecting the UX.
A bit of Googling lead me to find that I may need to memoize firstVisit.app but I'm not entirely sure how or the logic behind doing so?
React's log message tried to say that in your first useEffect hook callback function body you used some value that excluded in the list of dependencies. That's correct, please refer: useEffect Hook
The array of dependencies is not passed as arguments to the effect
function. Conceptually, though, that’s what they represent: every
value referenced inside the effect function should also appear in the
dependencies array. In the future, a sufficiently advanced compiler
could create this array automatically.
We recommend using the exhaustive-deps rule as part of our
eslint-plugin-react-hooks package. It warns when dependencies are
specified incorrectly and suggests a fix.
So, add to the second array parameter of useEffect firstVisit.app, and then check whether this warning/error is gone or not.
Also, you don't need to memoize primitive values, like boolean values (true/false), React is smart enough to not run your callback function again, when your boolean dependency hasn't changed after re-rendering.
[Edit]
If you want to run some logic in useEffect except initial render, i.e. to skip the first time render, you can use:
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
doYourTransition();
} else {
isMounted.current = true;
}
}, [deps]);
More complicated one with custom hook, but can be helpful based on your needs and wishes:
const useEffectAfterMount = (cb, deps) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
cb();
} else {
isMounted.current = true;
}
}, deps);
}
useEffectAfterMount(() => {
// the logic here runs always, but not on initial render
}, [firstVisit.app]);
I think, you got the idea. Of course, you can move these hooks into a separate file and to reuse them across the project and minimize the amount of code in your current component as well.

Nextjs: set priority for useEffect

This is my _app.tsx
useEffect(() => {
console.log(1)
}, []);
And this is my index.tsx
useEffect(() => {
console.log(2)
}, []);
So, in my console print 21
But I want to print 12
How can I set priority for useEffect for run _app first?
Short Answer
You can't set priority across components.
Long Answer
Disregarding why someone would need control over the priority of useEffect across components in the first place, the order at which useEffect are being run by different component, cannot be controlled on our end. Following this article,
useEffect runs the clean up and the new effect before moving to the
next component (depth) first and doing the same.
This means that children components will have there useEffect ran first, followed by its parent, which explains why the useEffect in index.tsx executes before app.tsx. This is because app.tsx can be seen as a container encompassing the whole app, making it the parent of any other component.
However, a possible workaround to control the order of a useEffect, is only by skipping a useEffect on first render by using a flag, further explained here.
const notInitialRender = useRef(false)
useEffect(() => {
if (notInitialRender.current) {
document.title = `Current count is ${count}`
} else {
notInitialRender.current = true
}
}, [count])

React component reacts unexpectedly

The following is a simple react component:
import React from "react";
import { useState } from "react";
export default function Comp() {
let [count, setCount] = useState(1);
function countUp(){
setCount(count + 1);
}
setInterval(countUp, 1000);
return <h2>{count}</h2>
}
I expected the counter to go up every second
But for some reason, after ten - twenty seconds something starts to go wrong
See here:
https://stackblitz.com/edit/react-az7qgn?file=src/comp.jsx
Can anyone explain this?
You should use useEffect hook to set up that properly. I can provide an example.
import React, { useState, useEffect } from "react";
export default function Comp() {
const [count, setCount] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
setCount(state => state + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <h2>{count}</h2>
}
A couple of notes.
In general, you would prefer const over let, but this is mandatory when destructuring things coming from React.
I suggest to read Using the Effect Hook on React docs to more information about useEffect.
Basically, useEffect allows you to achieve similar results to componentDidMount and componentDidUpdate lifecycle methods for class components. Also, in this specific case, by returning a function in useEffect callback, we make sure to clear the scheduled callback when it's time to clean up, which means after each run. This actually avoids the mess of stacking many setInterval on top of each other.
Also, when you setCount it's preferable to get the previous state by using the callback form, because that will be always up-to-date.
When calling setInterval(), it returns an interval id. Your code is not saving the variable, and thus you cannot reset it. On smaller iterations, you will not see the changes for every iteration. But, as the number of times that setInterval() is called increases from 0 to N, more timers are being initiated, and you will rapidly see flashes of numbers as they increase, because every interval is changing the state of count.
In other words, you are creating more and more timers as time goes on, rather than creating timers for one-time use. You will need to call clearInterval(timer_id_goes_here) to clear the timer. See code examples in the link below.
https://www.w3schools.com/jsref/met_win_setinterval.asp

Triggering useEffect only certain conditions

I have basic understanding of useEffect. Without second parameter (dependency array) it runs on every render. With empty array, it runs on first render. With parameters in array, it runs whenever some of parameters changes.
Say I have useEffect with two dependencies (from GraphQL query): result.data and result.loading. I want useEffect to run if result.data changes, and result.loading is false. Purpose is for example to update Redux store:
useEffect(() => {
if (result.loading) return;
dispatch(updatePhotos([...photos, ...result.data.photos]));
}, [result.data, result.loading]);
But there's a catch: I have to include photos to list of dependencies. However, photos variable will be updated in other place, and it triggers this useEffect again.
How can I run useEffect only when those two variables changes?
I can of course use useState to store variable resultFetched, set it to true in useEffect and then dispatch only if it is false. But at some point I have to change it back to true, and useEffect runs again, since I can't manually change result.data or result.loading.
I'm lost how to properly use useEffect in these situations when there is lots of variables to handle.
Currently I'm building infinite scrolling photo list, where list is loaded part by part via GraphQL. But when user opens some photo and eventually returns to photo list, it is restored from Redux to same state and scroll position as it was before opening the photo.
I have spent countless hours trying to get it work, but this useEffect-thing is spoiling my every attempt. :) They always gets triggered before I want them to trigger, because there is so many changing variables.
Also, sometimes I want to run a function within useEffect (function added to dependency array), and I use useCallback for that function to memoize it. But then I also have to add all variables that function uses to dependency array of that useCallback, so function gets regenerated when those variables changes. That means that useEffect suddenly runs again, because the function in dependency array changes.
Is there really no way to use functions/variables in useEffect, without them to trigger useEffect?
It all depends on how updatePhotos works. If that creates an action then the problem is you are creating the new state in the wrong place. The previous value of photos shouldn’t be used here because as you pointed out, that causes a dependency.
Instead your reducer will have the old value of photos you can use and you simply pass the new request data to your reducer.
Described in more detail here: https://overreacted.io/a-complete-guide-to-useeffect/#decoupling-updates-from-actions
You can have two separate useEffect functions inside the same component and they will work independent one of another. use one for photos and one for data loading. I hope this example helps you to wrap your head around this.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
useEffect(() => {
const id = setInterval(() => {
setCount2((c) => c + step);
}, 1500);
return () => clearInterval(id);
}, [step]);
return (
<div>
<h1>{count}</h1>
<h1>{count2}</h1>
<input value={step} onChange={(e) => setStep(Number(e.target.value))} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));
Please refer to this example in sandbox
https://codesandbox.io/s/react-playground-forked-6h0oz

how to use setInterval without useEffect in react

if i used setInterval(line 15) without useEffect than it gives result 2^n-1(0,1,3,7,15,31,63...) instead of(0,1,2,3,4,..) . so i have some question
1)why i am getting that output when I directly called setInterval without useeffect 2)is there any way if I change setCount(line 9) and its gives correct output by use setInterval directly without useEffect(as I did)
3) if the use of setInterval is not possible without useEffcet than why it is not possible?
if i put setInterval in useEffect and render initially once( line 12,13,14) than it gives correct output.....
but I do not get the correct output when I use directly setInterval. what is diff bet them?
in both cases, I call setInterval once but the output is diff.
import React, {useEffect, useState } from 'react'
export default function IncorrectDependency() {
const [count,setCount]=useState(0)
const inc=()=>{
// console.log(count)
setCount(preVal=>preVal+1)
// setCount(count+1)
}
// useEffect(()=>{
// setInterval(inc,1000)},[]
// )
setInterval(inc,1000)
return (
<div>
<h1>{count}</h1>
</div>
)
}
When we do a set state, functional components will re-execute from top to bottom, how ever when we use useState, useCallbacks etc.. they will not re-initialize as variables, functions,
So in this case, setInterval will re-initialize on each and every setCount, because of the state got changed,
step by step
in the 1st second there will be one setInterval, and call setCount and component is ready to rerender
when re-redering, start executing functional component from top-to-bottom it sees setInterval again and it will trigger it, so now we have two setIntervals
so on it will add multiple setIntervals on each second, because we don't clear it, so you should see the number printed in the browser will not take a second, but less than a second when time goes by.
You can achieve the expected result without useEffect by clearing the previous interval on each re-render which is happen due to setCount
create a variable to hold the set interval, code
const interval = null;
//this should be declare out side the component,
//because if we declare it inside the component it will redeclare,
//and the reference to the previous setInterval will be lost in that case no-way to clear the setInterval.
export default function IncorrectDependency() {
....
if (interval) {
clearInterval(interval);
}
interval = setInterval(inc, 1000);
....
}
alternatively react has a hook which can hold the same variables without re-initializing on each renderings, check it out useRef
here is a code-demo
const intvl = useRef(null);
....
if (intvl?.current) {
clearInterval(intvl.current);
}
intvl.current = setInterval(inc, 1000);
.....
when you directly use setInterval what is happening as this is a function it will be called on state change so again a setInterval will be triggered and so on which actually give you the incorrect result, so you shouldn't use setInterval without use effect, also on unmount you should clearthe interval
Dan Abramov explains why this isn't a good idea:
"it’s not the idiomatic way to do it. eg it won’t work correctly if you have multiple instances of the same component. it breaks the rules — it does a side effect (setInterval) during rendering, which the page said you should not do :) once you break the rules, all bets are off"
https://twitter.com/dan_abramov/status/1577395235340095501?s=20&t=nNnYyjLvHs5by_dqF5l8zg

Categories

Resources