I'm trying to update state in useEffect hook, but I came up with a problem with it. Inside the hook I have if statement where I'm setting the data in opacityBar variable, and outside the if i need to update the state with that variable, but it's not working. This is the code I have:
React.useEffect(() => {
let opacityBar;
if(filteredData.length > 0) {
const inc = (name) => filteredData.find((f) => f.name === name) !== undefined;
opacityBar = coloredBar?.data?.map((bar: any) => ({ ...bar, opacity: inc(bar.name) ? 1 : 0.333 }));
} else {
opacityBar = coloredBar?.data?.map((bar: any) => ({ ...bar, opacity: 1 }));
}
setColoredBar(opacityBar);
}, [filteredData, coloredBar]);
I've also tried to set state like this setColoredBar({ ...coloredBar, opacityBar }); but this causing an infinite loop. What am I doing wrong here?
You shouldn't be adding coloredBar as a dependency to useEffect because you are setting the same state in it. Doing this will lead to an infinite loop.
You can instead use functional setState like below
React.useEffect(() => {
setColoredBar(coloredBar => {
let opacityBar;
if(filteredData.length > 0) {
const inc = (name) => filteredData.find((f) => f.name === name) !== undefined;
opacityBar = coloredBar?.data?.map((bar: any) => ({ ...bar, opacity: inc(bar.name) ? 1 : 0.333 }));
} else {
opacityBar = coloredBar?.data?.map((bar: any) => ({ ...bar, opacity: 1 }));
}
return opacityBar;
})
}, [filteredData]);
Because you setColoredBar(opacityBar); inside the effect, and coloredBar is one of the triggers to run the effect, each time you finish the effect you start it again. just add checker to setColoredBar(opacityBar);
opacityBar != coloredBar && setColoredBar(opacityBar);
or if you don't really need coloredBar to be a trigger for re-run the effect, remove it from [dependencies] of the effect
Related
I want to do a recursive function which basically runs each time a folder has a subfolder under it, so I can take all the content from all the subfolders available.
Not sure what I am missing here, but the state change of subFolders does not trigger the useEffect which have it as dependency:
const [imageList, setImageList] = useState([]) as any;
const [subFolders, setSubFolders] = useState([]) as any;
const getFilesFromFolder = (fileId: string) => {
let noToken = false;
const requestFunction = ((pageToken?: string) => {
gapi.client.drive.files.list({
q: `'${fileId}' in parents`,
pageToken
}).execute((res: any) => {
const token = res.nextPageToken && res.nextPageToken || null;
const images = res.files.filter((file: any ) =>
file.mimeType === 'image/jpeg' ||
file.mimeType === 'image/png' ||
file.mimeType === 'image/jpg'
);
setSubFolders([...subFolders, ...res.files.filter((file: any ) => file.mimeType === 'application/vnd.google-apps.folder')]);
setImageList([...imageList, ...images])
if (token) {
requestFunction(token);
} else {
noToken = true;
}
}).catch((err: any) => {
console.log('err', err)
})
});
if (!noToken) {
requestFunction();
}
}
useEffect(() => {
if (subFolders && subFolders.length > 0) {
subFolders.forEach((subFolder: any) => {
getFilesFromFolder(subFolder.id);
});
}
}, [subFolders])
Since you are basically looping over file structure and enqueueing state updates I'm going to just assume any issues you have are because you are using normal state updates. When these are enqueued in loops or multiple times within a render cycle they overwrite the previous enqueued update.
To resolve you should really use functional state updates. This is so each update correctly updates from the previous state, and not the state from the previous render cycle. It's a subtle but important difference and oft overlooked issue.
Functional Updates
setSubFolders(subFolders => [
...subFolders,
...res.files.filter((file: any ) =>
file.mimeType === 'application/vnd.google-apps.folder'
),
]);
setImageList(imageList => [...imageList, ...images]);
I'm trying to change the color of a SVG structure from red to white and white to red using React Hooks. The problem is that after a few seconds the color starts changing rapidly instead of every second. I'm not understanding where I'm going wrong in the code.
Here is the useState.
const[rectColor, rectColorSet] = useState('red')
And the useEffect for changing the color.
useEffect(() => {
if(rectColor === 'red'){
let timer = setInterval(() => {
rectColorSet('white')
}, 1000)
}
// console.log('timer',timer)
else if(rectColor ==='white'){
let timer = setInterval(() => {
rectColorSet('red')
}, 1000)
}
})
And this is the place where I use the state which contains color.
d={`
M-0.20 0 L-0.20 0.30 L0.20 0.30 L0.20 0 L-0.20 0
Z`}
fill={props.value ? "green" : rectColor}
/>
}
Every render's setting a new interval. Use setTimeout instead of setInterval, and don't forget to clean up the effects:
useEffect(() => {
if (rectColor === 'red' || rectColor === 'white') {
const timeoutID = setTimeout(() => {
rectColorSet(rectColor === 'red' ? 'white' : 'red');
}, 1000);
return () => clearTimeout(timeoutID);
}
});
Or toggle between a boolean instead of using a string for state. If you add other state that might change, using a single interval when the component mounts might be easier to manage:
const [red, setRed] = useState(true);
useEffect(() => {
const intervalID = setInterval(() => {
setRed(red => !red);
}, 1000);
return () => clearInterval(intervalID);
}, []);
After a few seconds, the color starts changing rapidly instead of every second because you put [] in useEffect.
If you want to run an useEffect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as the second argument in useEffect hook.
And You should use setTimeout() intend of setInterval()
So you should write your useEffect like this:
const [red, setRed] = useState(true);
useEffect(() => {
const intervalID = setTimeout(() => {
setRed(red => !red);
}, 1000);
return () => clearInterval(intervalID);
}, []);
You actually need to replace setInterval() with setTimeout()
and rather than declaring everthing seperately use OR in the conditional and clean up the code.
I have the following code:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, [ddFilterData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}
What I need is for useEffect to execute eah time ddFilerData changes with NEW or DIFFERENT data.
From my understanding it should only update or run when teh ddFilterData is different no?
Currently it runs on each change. The code above enters into an infinite loop even thou filteredData isn't different. Any ideas what I'm doing wrong?
Your returnedData is an array. So when you do setddFilterData(returnedData) you're setting a new value for ddFilterData. Because React uses Object.is for comparison, even if the array elements are the same as previously, it is still a different object and will trigger useEffect again, causing the infinite loop.
your getDropdownData method is updating ddFilterData which causes re-render. And on re-render you getDropdownData is called which updated ddFilterData due to this cyclic behavior your are getting infinte loop.
Modify your code like this:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, []);
useEffect(() => {
// put your code here if you want to do something on change of ddFilterData
}, [getDropdownData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}
The state doesn't update the value even though I'm setting it to the oldvalue + 1.
When logging out the values of ltrNewValue or rtlNewValue it's always the same. It's as it's being overwritten by the initial state.
const Row = (props) => {
const [rowState, setRowState] = useState({
renderInterval: null,
value: 0,
});
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
My end goal is to increment the rowState.value up to 2 and then setting it to 0 in a infinite loop. How do I do this correctly?
I'm not certain, but it looks like you have a problem with a stale callback here.
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
This effect only runs once - When the component is mounted the first time. It uses the counterIntervalFunction function for the interval:
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
The counterIntervalFunction captures the reference of props and uses it to determine what to display to the user. However, because this function is only run when the component is mounted, the event will only be run with the props passed to the function originally! You can see an example of this happening in this codesandbox.io
This is why you should put all external dependencies inside of the dependencies array:
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, [counterIntervalFunction, props.speed, rowState]);
However, this will cause an infinite loop.
Setting state in useEffect is usually considered a bad idea, because it tends to lead to infinite loops - changing the state will cause the component to re-render, causing another effect to be triggered etc.
Looking at your effect loop, what you're actually interested in is capturing a reference to the interval. This interval won't actually have any impact on the component if it changes, so instead of using state, we can use a ref to keep track of it. Refs don't cause re-renders. This also means we can change value to be a stand-alone value.
Because we now no longer depend on rowState, we can remove that from the dependencies array, preventing an infinite render. Now our effect only depends on props.speed and counterIntervalFunction:
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.speed);
return () => {
cancelInterval(renderInterval.current);
};
}, [props.speed, counterIntervalFunction]);
This will work, but because counterIntervalFunction is defined inline, it will be recreated every render, causing the effect to trigger every render. We can stablize it with React.useCallback(). We'll also want to add all the dependencies of this function to ensure that we don't capture stale references to props and we can change setRowState to setValue. Finally, because the interval is cancelled by useEffect, we don't need to call clearCounterInterval anymore.
const counterIntervalFunction = React.useCallback(() => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
props.setRotatingValue(props.index, ltrNewValue);
} else if (isRunning && props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
props.setRotatingValue(props.index, rtlNewValue);
}
}, [value, props]);
This can be simplified even further by moving the required props to the arguments:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
This could be even simpler if not for setRotatingValue: Right now, you have a component that both maintains it's own state and tells the parent when its state changes. You should be aware that the component state value might not necessarily update when you call it, but setRotatingValue absolutely will. This may lead to a situation where the parent sees a different state than the child does. I would recommend altering the way your data flows such that it's the parent that owns the current value and passes it via props, not the child.
This gives us the following code to finish off:
function Row = (props) => {
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.isRunning, props.direction, props.setRotatingValue, props.index);
return () => {
cancelInterval(renderInterval.current);
};
}, [props, counterIntervalFunction]);
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
...
}
In this code, you'll notice that we run the effect every time the props or the function changes. This will mean that, unfortunately, the effect will return every loop, because we need to keep a fresh reference to value. This component will always have this problem unless you refactor counterIntervalFunction to not notify the parent with setRotatingValue or for this function to not contain its own state. An alternatively way we could solve this would be using the function form of setValue:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
setValue(value => {
if (direction === 'ltr') {
return value === 2 ? 0 : value + 1;
} else if (direction ==' rtl') {
return value === 0 ? 2 : value - 1;
}
})
}, []);
Because the state update is not guaranteed to run synchronously, there's no way to extract the value from the setValue call and then call the setRotatingValue function, though. :( You could potentially call setRotatingValue inside of the setValue callback but that gives me the heebie geebies.
It's an interval and it may mess things up when you call setState directly by relying on the old state by the name rowState, try this:
setRowState(oldstate=> { ...rowState, value: oldstate.value+1 });
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