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 });
Related
The aim is to set the initial state:
if the StateColumn is visible/applicable (true)
and it should be accepted or waiting_for_review or rejected (or nothing if the StateColumn is not shown), depending on this order if they exist
This is what I did:
const [selectedStuffState, setSelectedStuffState] = useState( showStateColumn? ('accepted' || 'waiting_for_review' || 'rejected') : '' );
Is there a better way of doing this?
You will need 1 function to be run on every state change of stateColumn. Where will set the state of setSelectedStuffState. But for that also, you will require 1 more state which will decide the optional values of your state.
The logic may work like this
const YourComponent = (props) => {
const { showStateColumn, status } = props;
const [selectedStuffState, setSelectedStuffState] = useState("");
useEffect(() => {
if (showStateColumn) {
// now here you will require condition to check if the selectedStuffState is 'accepted' || 'waiting_for_review' || 'rejected'
// you will require 1 more status prop which will decide the state of selectedStuffState
if (status === "accepted") {
setSelectedStuffState("");
} else if (status === "waiting_for_review") {
setSelectedStuffState("waiting_for_review");
} else {
setSelectedStuffState("rejected");
}
}
}, [showStateColumn]); //useEffect will run on every change of showStateColumn
};
const setInitalState = () => {
return showStateColumn? ('accepted' || 'waiting_for_review' || 'rejected') : ''
}
const [selectedStuffState, setSelectedStuffState] = useState(setInitalState());
I have a react template to generate the hand gesture using tensorflow.js. The current code looks like this
const [curstep, setCurstep] = useState(0);
const detect = async (net) => {
const hand = await net.estimateHands(video);
const gesture = await net.estimate(hand[0].landmarks, 4);
....
if (curstep === 0 && gesture === "Number 1")
setCurstep(1);
else if (curstep === 1 && gesture === "Number 2")
setCurstep(2);
else if (curstep === 2 && gesture === "Number 3")
setCurstep(3);
....
}
const runHandpose = async () => {
const net = await handpose.load();
setInterval(() => {detect(net)}, 30);
}
useEffect(() => {runHandpose()}, []);
Here my idea is to use curstep to track the gesture, and when user sequentially does the gesture "1" and "2" and "3", the app would launch some other UI components. But I had some issues with the state curstep is not updating. After some debugging I realize that if I remove the curstep === 0 condition (and curstep === 1 etc) it seems that the curstep can be updated.
Therefore I am wondering if this is because I am checking the state and updating state in a async function, and what should I do to circumvent this issue. Thank you.
useEffect(() => {runHandpose()}, []);
Because of the empty dependency array, this effect is run only once, when the component mounts. Whatever value the const curstep had at the time the interval was set up, that's what it will be when the interval goes off.
From the code you provided, it looks like all you need to do to fix this is to use the function version of setCurstep, so it can pass you the latest value. Though let me know if something in the code you omitted makes this insufficient.
const detect = async (net) => {
const hand = await net.estimateHands(video);
const gesture = await net.estimate(hand[0].landmarks, 4);
setCurstep(prev => {
....
if (prev === 0 && gesture === "Number 1") {
return 1;
} else if (prev === 1 && gesture === "Number 2") {
return 2;
} else if (prev === 2 && gesture === "Number 3") {
return 3;
}
....
});
}
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
In my current I am was trying to check if the latest index (number 5) jumps to 1. Since I have built a function counter that automatically jumps to 1 when it reach the latest index, but I also want to have a check when it jumps from latest index to the first index...React Hook not necessarily needed for this issue....
const App = ({ scoreCounter }) => {
const boolean = useRef(null);
const [ checkCounter, setCheckCounter ] = useState(false);
useEffect(() => {
const storedCounter = currentCounter;
boolean.current = storedCounter;
return () => storedCounter;
}, []);
useEffect(() => {
if(currentCounter == 5) {
}
console.log(boolean.current, currentCounter);
}, [boolean.current, currentCounter])
}
const mapStateToProps = state => {
return {
currentCounter: state.game.counter
}
}
If you're using class components you can compare your prevState to your current state in the componentDidUpdate function. In hooks you can implement something similar with the example shown here: usePrevious.
Using the usePrevious function in the link you can do this:
const prevCount = usePrevious(checkCounter);
// Hook
function usePrevious(value) {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
useEffect(() => {
if (prevCount === 5 && checkCounter === 1) {
// your code here
}
}, [prevCount, checkCounter])
const Navbar = () => {
const prevScrollY = React.useRef<number>();
const [isHidden, setIsHidden] = React.useState(false);
React.useEffect(() => {
const onScroll = () => {
const scrolledDown = window.scrollY > prevScrollY.current!;
console.log(`is hidden ${isHidden}`);
if (scrolledDown && !isHidden) {
setIsHidden(true);
console.log(`set hidden true`);
} else if (!scrolledDown && isHidden) {
console.log(`set hidden false. THIS NEVER HAPPENS`);
setIsHidden(false);
}
prevScrollY.current = window.scrollY;
};
console.log("adding listener");
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
return isHidden ? null : <div>navbar</div>;
};
Full example
The console.log(`is hidden ${isHidden}`); always prints false, and the setIsHidden(true) always gets triggered but never seems to change the state. Why? Basically the isHidden is never setto false, except after the useState initialization.
Basically what happens is that your useEffect runs only twice on mount and on unmount (and that's apparently intentional), however the unwanted side-effect of this is that the value of isHidden that you're checking against in the onScroll method gets closured at it's initial value (which is false) - forever (until the unmount that is).
You could use functional form of the setter, where it receives the actual value of the state and put all the branching logic inside it. Something like:
setIsHidden(isHidden => { // <- this will be the proper one
const scrolledDown = window.scrollY > prevScrollY.current!;
console.log(`is hidden ${isHidden}`);
if (scrolledDown && !isHidden) {
console.log(`set hidden true`);
return true;
} else if (!scrolledDown && isHidden) {
console.log(`set hidden false. THIS NEVER HAPPENS`);
return false;
} else {
// ...