To restrict useEffect from running on the first render we can do:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
According to example here: https://stackoverflow.com/a/53351556/3102993
But what if my component has multiple useEffects, each of which handle a different useState change? I've tried using the isFirstRun.current logic in the other useEffect but since one returns, the other one still runs on the initial render.
Some context:
const Comp = () => {
const [ amount, setAmount ] = useState(props.Item ? Item.Val : 0);
const [ type, setType ] = useState(props.Item ? Item.Type : "Type1");
useEffect(() => {
props.OnAmountChange(amount);
}, [amount]);
useEffect(() => {
props.OnTypeChange(type);
}, [type]);
return {
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
}
}
The reason I've used separate useEffects for each is because if I do the following, it doesn't update the amount.
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
As far as I understand, you need to control the execution of useEffect logic on the first mount and consecutive rerenders. You want to skip the first useEffect. Effects run after the render of the components.
So if you are using this solution:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
useEffect (() => {
// second useEffect
if(!isFirstRun) {
console.log("Effect was run");
}
});
So in this case, once isFirstRun ref is set to false, for all the consecutive effects the value of isFirstRun becomes false and hence all will run.
What you can do is, use something like a useMount custom Hook which can tell you whether it is the first render or a consecutive rerender. Here is the example code:
const {useState} = React
function useMounted() {
const [isMounted, setIsMounted] = useState(false)
React.useEffect(() => {
setIsMounted(true)
}, [])
return isMounted
}
function App() {
const [valueFirst, setValueFirst] = useState(0)
const [valueSecond, setValueSecond] = useState(0)
const isMounted = useMounted()
//1st effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueFirst ran")
}
}, [valueFirst])
//2nd effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueSecond ran")
}
}, [valueSecond])
return ( <
div >
<
span > {
valueFirst
} < /span> <
button onClick = {
() => {
setValueFirst((c) => c + 1)
}
} >
Trigger valueFirstEffect < /button> <
span > {
valueSecond
} < /span> <
button onClick = {
() => {
setValueSecond((c) => c + 1)
}
} >
Trigger valueSecondEffect < /button>
<
/div>
)
}
ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I hope it helps !!
You can use a single useEffect to do both effects in, you just implemented the logic incorrectly.
Your original attempt:
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
The issue here is the if/elseif, treat these as independent effects instead:
useEffect(() => {
if (amount !== 0) props.onAmountChange(amount);
if (type !== "Type1") props.onTypeChange(type);
}, [amount, type])
In this method if the value is different than the original value, it will call the on change. This has a bug however in that if the user ever switches the value back to the default it won't work. So I would suggest implementing the entire bit of code like this instead:
const Comp = () => {
const [ amount, setAmount ] = useState(null);
const [ type, setType ] = useState(null);
useEffect(() => {
if (amount !== null) {
props.onAmountChange(amount);
} else {
props.onAmountChange(0);
}
}, [amount]);
useEffect(() => {
if (type !== null) {
props.onTypeChange(type);
} else {
props.onTypeChange("Type1");
}
}, [type]);
return (
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
)
}
By using null as the initial state, you can delay calling the props methods until the user sets a value in the Radio that changes the states.
If you are using multiple useEffects that check for isFirstRun, make sure only the last one (on bottom) is setting isFirstRun to false. React goes through useEffects in order!
creds to #Dror Bar comment from react-hooks: skip first run in useEffect
Related
I'm trying to achieve smooth animation of searchbar (MUI Autocomplete). This should work only on Smartphones (Screen < 600px).
Here is an example (it is very buggy and open it on smartphone to see the animation): https://react-zxuspr-gjq5w8.stackblitz.io/
And here is my implementation, but I've a few problems with that:
The interval does not reset on dropdown close.
The React.useEffect() dependency is set to searchActive, which is changed dynamically.
I tried calling the callback function of React.useState(), but since the component is not destroyed, I am not sure if it makes sense.
The width of dropdown, which is also changed in the setInterval() function, is not smooth at all.
https://stackblitz.com/edit/react-zxuspr-gjq5w8?file=demo.js
Here is part of the component where the logic is implemented:
export function PrimarySearchAppBar() {
const [searchActive, setSearchActive] = React.useState(null);
const [acPaperWidth, setAcPaperWidth] = React.useState(null);
const [acPaperTransX, setAcPaperTransX] = React.useState(0);
const AcRef = React.useRef(null);
const isMobile = useMediaQuery(useTheme().breakpoints.down('sm'));
const options = top100Films.map((option) => {
const group = option.group.toUpperCase();
return {
firstLetter: group,
...option,
};
});
React.useLayoutEffect(() => {
if (AcRef.current) {
setAcPaperWidth(AcRef.current.offsetWidth);
}
console.log(acPaperWidth);
}, [AcRef]);
let interval;
React.useEffect(() => {
if (searchActive) {
if (acPaperTransX <= 39) {
interval = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
setAcPaperWidth((acPaperTransX) => acPaperTransX + 1);
if (acPaperTransX >= 38) {
clearInterval(interval);
}
console.log(acPaperTransX);
}, 10);
}
} else {
setAcPaperTransX(0);
clearInterval(interval);
}
}, [searchActive]);
return (
<>Hello World</>
);
}
The problem was that the interval variable was defined outside the React.useEffect() hook, so its value was not preserved on re-renders.
I was able to fix that by using React.useRef():
const intervalRef = React.useRef();
const acPaperTransXRef = React.useRef(acPaperTransX);
React.useEffect(() => {
if (searchActive) {
if (acPaperTransXRef.current <= 39) {
intervalRef.current = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
acPaperTransXRef.current += 0.1;
if (acPaperTransXRef.current >= 38) {
clearInterval(intervalRef.current);
acPaperTransXRef.current = 0;
}
console.log(acPaperTransXRef.current);
}, 2);
}
} else {
setAcPaperTransX(0);
acPaperTransXRef.current = 0;
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current);
}, [searchActive]);
I'm trying to update a React Component (B) that renders an SVG object passed from a parent Component (A).
B then uses getSVGDocument().?getElementById("groupID") and adds handling for events based on members of the SVG group.
A also passes in a prop that indicates mouseover in a separate menu.
Simplified code in B:
export function ComponentB(props: {
overviewSvg: string
highlightKey?: string
}) {
function getElems(): HTMLElement[] {
let innerElems = new Array<HTMLElement>()
const svgObj: HTMLObjectElement = document.getElementById(
"my_svg"
) as HTMLObjectElement
if (svgObj) {
const elemGroup = svgObj.getSVGDocument()?.getElementById("elemGroup")
if (elemGroup) {
for (let i = 0; i < elemGroup.children.length; i++) {
innerElems.push(elemGroup.children[i] as HTMLElement)
}
}
}
return innerElems
}
const elems = getElems() // Also tried variations with useState and useEffect, can't seem to get the right combination...
useEffect(() => {
console.log("effect called")
console.log(elems)
elems?.forEach((elem) => {
elem.onmousedown = () => toggleColor(elem)
})
}, [elems, props.overviewSvg])
useEffect(() => {
elems?.forEach((elem) => {
if (elem.id === props.highlightKey) {
setActive(elem)
} else {
setInactive(elem)
}
})
}, [elems, props.highlightKey, props.overviewSvg])
return (
<>
<object data={props.overviewSvg} id="my_svg" />
</>
)
}
I'm not sure what the appropriate parameters for the dependencies in useEffect should be, or if this is entirely the wrong approach?
The console shows that on loading, the array of elems is often empty, in which case the onmousedown loop of course doesn't add any functions.
After the parent sets the props.highlightKey, the second function is triggered and that then triggers the first effect and adds the mousedown functions.
What should I be changing to make the component correctly render before any parent highlightKey changes?
I was able to get the desired behaviour by using useRef and applying it to the DOM element, then adding an onload behaviour to that element.
// Get a ref for the <object> element
const svgObj = useRef<HTMLObjectElement>(null)
const [elemArr, setElems] = useState(Array<HTMLElement>())
if (svgObj.current) {
svgObj.current.onload = () => {
const elems = getElems()
elems.forEach((elem) => {
elem.onmousedown = () => toggleColor(elem)
})
setElems(elems)
}
}
useEffect(() => {
elemArr.forEach((elem) => {
if (elem.id === props.highlightKey) {
setActive(elem)
} else {
setInactive(elem)
}
})
}, [elemArr, props.highlightKey])
return (
<object ref={svgObj} data={props.overviewSVG} ... />
)
React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);
Hi I am new developer at ReactJs. I have a problem about useEffect rendering. I have an example and i am trying to change background image with time but useEffect make rendering so much and my background image is staying in a loop with time. I just want to change my images with order like bg1 bg2 bg3 etc.
How can I solve this infinite render?
my example .tsx
import React, { useEffect, useState } from 'react';
import { connect } from "../../../store/store";
const LeftPart = (props: any) => {
const [value, setValue] = useState(1);
const {loginLeftImagesLength} = props;
const changeLeftBackgroungImage : any = () =>{
const interval = setInterval(() => {
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
}, 3000);
return () => clearInterval(interval);
};
useEffect(() => {
changeLeftBackgroungImage();
});
return (
<div className="col-xl-7 col-lg-7 col-md-7 col-sm col-12">
<img id="image" src={"../../../assets/images/bg"+value+".jpg"} style={{ width: "100%", height: "99vh" }} alt="Login Images"></img>
</div >
)
}
export default connect((store: any) => ({ loginLeftImagesLength: store.loginLeftImagesLength }))(LeftPart) as any;
You have infinite render as you have not specified any dependencies in your useEffect
useEffect(() => {
changeLeftBackgroungImage();
},[]); // Will run this hook on component mount.
Also you could do it in this way
useEffect(()=>{
const timer = setTimeout(()=>{
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
},3000)
return ()=>{ // Return this function from hook which is called before the hook runs next time
clearTimeout(timer)
}
},[,value]) // RUN THIS HOOK ON componendDidMount and whenever `value` changes
Why not put this entire code inside the useEffect hook.
useEffect(() => ) {
const {loginLeftImagesLength} = props;
const changeLeftBackgroungImage : any = () =>{
const interval = setInterval(() => {
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
}, 3000);
return () => clearInterval(interval);
};[changeBGStateHere])
use if else statements to change the background...
if (value === 1) {
changeLeftBackgroungImage();
} else (value === 2) {
and so on.
Let the interval change the state and let useEffect rerender when the state for the time changes.
using the following code to rotate an array of object through a component DOM. The issue is the state never updates and I can't workout why..?
import React, { useState, useEffect } from 'react'
const PremiumUpgrade = (props) => {
const [benefitsActive, setBenefitsActive] = useState(0)
// Benefits Details
const benefits = [
{
title: 'Did they read your message?',
content: 'Get more Control. Find out which users have read your messages!',
color: '#ECBC0D'
},
{
title: 'See who’s checking you out',
content: 'Find your admirers. See who is viewing your profile and when they are viewing you',
color: '#47AF4A'
}
]
// Rotate Benefit Details
useEffect(() => {
setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [])
the output I get looks like the following image. I can see the useState 'setBenefitsActive' is being called but 'benefitsActive' is never updated.
You pass no dependencies to useEffect meaning it will only ever run once, as a result the parameter for setInterval will only ever receive the initial value of benefitsActive (which in this case is 0).
You can modify the existing state by using a function rather than just setting the value i.e.
setBenefitsActive(v => v + 1);
Some code for your benefit!
In your useEffect as #James suggested, add a dependency to the variable that's being updated. Also don't forget to clean up your interval to avoid memory leaks!
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
//Clean up can be done like this
return () => {
clearInterval(rotationInterval);
}
}, [benefitsActive]) // Add dependencies here
Working Sandbox : https://codesandbox.io/s/react-hooks-interval-demo-p1f2n
EDIT
As pointed out by James this can be better achieved by setTimeout with a much cleaner implementation.
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setTimeout(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [benefitsActive]) // Add dependencies here
Here, a sort of interval is created automatically due to the useEffect being called after each setTimeout, creating a closed loop.
If you still want to use interval though the cleanup is mandatory to avoid memory leaks.
When you pass a function to setInterval, you create a closure, which remembers initial value of benefitsActive. One way to get around this is to use a ref:
const benefitsActive = useRef(0);
// Rotate Benefit Details
useEffect(() => {
const id = setInterval(() => {
console.log(benefits.length);
console.log(benefitsActive.current);
if (benefitsActive.current >= benefits.length) {
console.log("................................. reset");
benefitsActive.current = 0;
} else {
console.log("................................. increment");
benefitsActive.current += 1;
}
}, 3000);
return () => clearInterval(id);
}, []);
Demo: https://codesandbox.io/s/delicate-surf-qghl6
I've had the same problem and found a perfect solution on
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
using an own hook
import { useRef, useEffect } from "react";
export function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
using it like
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);