Same logic but different behaviour in 'class' and in 'functional component' - javascript

Attempted to translate an example code from class to functional component and faced the problem.
the target file is in components/Wheel/index.js
Key function that causes problem
const selectItem = () => {
if (selectedItem === null) {
const selectedItem = Math.floor(Math.random() * items.length);
console.log(selectedItem);
setSelectedItem(selectedItem);
} else {
setSelectedItem(null);
let t= setTimeout(() => {
selectItem()
}, 500);
clearTimeout(t);
}
};
First time is normal,
from second time onward,
2 clicks are needed for the wheel to spin.
I had to add clearTimeout() or infinite loop is resulted, but the same does not happen in the original.
Original working example in class
My version in functional component.
MyVersion
Thank you.

What an excellent nuance of hooks you've discovered. When you call selectItem in the timeout, the value of selectedItem that is captured in lexical scope is the last value (not null).
There's two answers, a simple answer and a better working answer.
The simple answer is you can accomplish it be simply separating the functions: https://codesandbox.io/s/spinning-wheel-game-forked-cecpi
It looks like this:
const doSelect = () => {
setSelectedItem(Math.floor(Math.random() * items.length));
};
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
setTimeout(doSelect, 500);
}
};
Now, read on if you dare.
The complicated answer fixes the solution for the problem if items.length may change in between the time a timer is set up and it is fired:
https://codesandbox.io/s/spinning-wheel-game-forked-wmeku
Rerendering (i.e. setting state) in a timeout causes complexity - if the component re-rendered in between the timeout, then your callback could've captured "stale" props/state. So there's a lot going on here. I'll try and describe it as best I can:
const [selectedItem, setSelectedItem] = useState(null);
// we're going to use a ref to store our timer
const timer = useRef();
const { items } = props;
// this is just the callback that performs a random select
// you can see it is dependent on items.length from props
const doSelect = useCallback(() => {
setSelectedItem(Math.floor(Math.random() * items.length));
}, [items.length]);
// this is the callback to setup a timeout that we do
// after the user has clicked a "second" time.
// it is dependent on doSelect
const doTimeout = useCallback(() => {
timer.current = setTimeout(() => {
doSelect();
timer.current = null;
}, 500);
}, [doSelect]);
// Here's the tricky thing: if items.length changes in between
// the time we rerender and our timer fires, then the timer callback will have
// captured a stale value for items.length.
// The way we fix this is by using this effect.
// If items.length changes and there is a timer in progress we need to:
// 1. clear it
// 2. run it again
//
// In a perfect world we'd be capturing the amount of time remaining in the
// timer and fire it exactly (which requires another ref)
// feel free to try and implement that!
useEffect(() => {
if (!timer.current) return;
clearTimeout(timer.current);
doTimeout();
// it's safe to ignore this warning because
// we know exactly what the dependencies are here
}, [items.length, doTimeout]);
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
doTimeout();
}
};

Related

set time dynamic time for set interval in react

Hello i am struggling to set dynamic time for settimeout function in react js.
i have long string of key value pair of time and message. i wants to display each message for specific time and loop through whole list.
here is what i am trying, but not working.
const [timer, setTimer] = useState(0)
const [time, setTime] = useState(5000)// this is default value to start which need to update with str time value
const str=[{name:"rammy", time:1000},
{name:"james", time:4000},
{name:"crown", time:2000}]
useEffect(()=>{
const getTime= str[timer].time
setTime(getTime)
},[timer])
//when timer change it should update update time state which will be used to update time for time settime out
function increment() {
useEffect(()=>{
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
},[timer])
} // above code is for increment time state on each iteration
function ButtonHandle(){
//setRealString(itr)
increment()
} //button handler for start timer
First of all, you can't put hooks inside functions (other than your component functions). https://reactjs.org/docs/hooks-rules.html
So take the useEffect out of increment()
useEffect(()=>{
increment()
},[timer])
function increment() {
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
}
But you also need to clear the timeout. We can return the timeout function to reference it, and clear the time out with a return inside useEffect. Clearing timeouts and intervals in react
useEffect(()=>{
const myTimeout = increment()
return () => {
clearTimeout(myTimeout)
}
},[timer])
function increment() {
return setTimeout(() => {
setTimer((ele) => ele + 1);
}, time);
}
Then we can combine the useEffects which both have a dependancy array of [timer].
useEffect(() => {
const getTime = str[timer].time;
setTime(getTime);
const myTimeout = increment();
return () => {
clearTimeout(myTimeout);
};
}, [timer]);
You don't need to use useEffect to do it. You misunderstood the useEffect usage, it's a react hook to you implement side-effects and you can't use react-hooks inside a function, it should be in the component scope.
I can increment directly from the ButtonHandle function.
// On the index state is necessary in this implementation
const [index, setIndex] = useState(-1)
const guys=[
{name: "rammy", time:1000},
{name: "james", time:4000},
{name: "crown", time:2000}
]
// useCallback memoize the increment function so that it won't
// change the instance and you can use it in the useEffect
// dependency array
const increment = useCallback(() => {
setIndex((i) => i+1)
}, [])
useEffect(() => {
// change the state if the timer is greater than -1
if (index !== -1) {
if (index >= guys.length) {
setIndex(-1);
} else {
setTimeout(() => {
increment();
}, guys[index].time); // <-- you get the time from your array
}
}
}, [index, increment]);
function handleClick(){
//setRealString(itr)
increment()
}
Even though I helped you, I don't know what you're trying to do. This implementation sounds like a code smell. We can help you better if you explain the solution you're trying to do instead of just the peace of code.
You don't need to set the time state, as you already have the time in the array; avoiding unnecessary state changes is good.

ReactJS counter or timer with Start or Stop Button

Need help to understand in useEffect, if I don't put counter and timerCheck in useEffect dependency then what it will effect here.
And if I put timerCheck dependency in useEffect then counter increasing 100 time faster
Also how can i run this code without any error or warning
code in codesandbox
const [counter, setCounter] = useState(0);
const [startcounter, setStartcounter] = useState(false);
const [timerCheck, setTimerCheck] = useState(0);
useEffect(() => {
let intervalStart = null;
if (startcounter) {
intervalStart = setInterval(() => {
setCounter((prevState) => (prevState += 1));
}, 1000);
setTimerCheck(intervalStart);
} else {
clearInterval(timerCheck);
}
return () => {
if (counter !== 0) clearInterval(timerCheck);
};
}, [startcounter]);
const handleStartButton = () => {
setStartcounter(true);
};
const handleStopButton = () => {
setStartcounter(false);
};
Try not to declare unnecessary states. timerCheck state is redundant.
You want to start an interval that increases counter by one every second.
When user stops the stopwatch , you want the interval to be cleared, so it would stop increasing the timer.
So your only dependancy in useEffect would be whether your stopwatch is running or not. And the only thing that matters to you, is when startCounter is ture. You start an interval when it is true and clear the interval in its cleanup.
useEffect(() => {
let intervalStart = null;
if (!startcounter) {
return () => {};
}
intervalStart = setInterval(() => {
setCounter((prevState) => (prevState += 1));
}, 1000);
return () => {
clearInterval(intervalStart);
};
}, [startcounter]);
Here is the codesandbox
But you do not really need useEffect for this. IMO it would be a much better code without using useEffect.
So useEffect with no dependency will run on every render.
If you include a dependency, it will re-render and run whenever that dependency changes. So by including timerCheck in it, you're telling the page to re-render whenever that changes, which ends up changing it, causing it to re-render again, causing it to change again, etc., etc.
More info: https://www.w3schools.com/react/react_useeffect.asp

SetInterval causes too many re-renders React

Hello I would like to put setInterval in my React project to add 1 for each second but I got an error like in title of this post.
js:
const [activeTab, setActiveTab] = useState(0)
useEffect(() => {
setInterval(setActiveTab(prevTab => {
if (prevTab === 3) return 0
console.log('hi')
return prevTab += 1
}), 1000)
})
There are a few issues:
You're not passing a function to setInterval, you're calling setActiveTab and passing its return value into setInterval.
If you were passing in a function, you'd be adding a new repeated timer every time your componennt ran. See the documentation — useEffect:
By default, effects run after every completed render...
And setInterval:
The setInterval() method... repeatedly calls a function or executes a code snippet, with a fixed time delay between each call.
(my emphasis)
Starting a new repeating timer every time your component re-renders creates a lot of repeating timers.
Your component will leave the interval timer running if the component is unmounted. It should stop the interval timer.
To fix it, pass in a function, add a dependency array, and add a cleanup callback to stop the interval:
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
const handle = setInterval(() => { // *** A function
setActiveTab(prevTab => {
if (prevTab === 3) return 0;
console.log("?ghi");
return prevTab += 1;
});
}, 1000);
return () => { // *** Clear the interval on unmount
clearInterval(handle); // ***
}; // ***
}, []); // *** Empty dependency array = only run on mount
Side note: Assuming you don't need the console.log, that state setter callback function can be simpler by using the remainder operator:
setActiveTab(prevTab => (prevTab + 1) % 3);
useEffect(() => {
setInterval(() => {
setActiveTab((prevTab) => {
if (prevTab !== 3) {
return (prevTab += 1);
} else {
return 0;
} // console.log("hi");
});
}, 1000);
}, []);

JavaScript On-click Function to start and stop Intervals

I have two onclick functions that performing start and stop functions using socket when i click start it start requesting images from server(i am using intervals) when i click stop the server is stopped but the request keep coming to the server. the intervals seems not stopped after clicking kindly review my code
Code:
var interval;
function onclick() {
interval = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350)
}
function onclicks() {
clearInterval(interval);
setImage(blanked);
}
I have tried to add clearInterval function but it seems not working
Variables declared at the top level of a component are redeclared on each render (here every time you call setImage()) so interval is undefined by the time onclicks() is called.
In order to maintain the value through renders you need to either use state (useState) or a ref (useRef) which won't lose it's value. In this case, since you don't want renders attached to the value of interval a ref is more appropriate. useRef() returns an object with a current property in which your value will be stored.
const interval = React.useRef(null);
function onclick() {
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350);
}
function onclicks() {
clearInterval(interval.current);
interval.current = null; // reset the ref
setImage(blanked);
}
You probably also want to avoid setting multiple intervals. You may already be doing so, but if not you can check if interval is null or not before setting a new one.
const interval = React.useRef(null);
function onclick() {
if (interval.current === null) { // only set new interval if interval ref is null
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350);
}
}
For me, the easiest way to solve this behavior was set the interval as a Ref:
import { useRef } from 'react';
// Initialize the interval ref
const interval = useRef();
// Create the interval
function onclick() {
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350)
}
// Remove the interval
function onclicks() {
clearInterval(interval.current);
setImage(blanked);
}
Using this hook, the component will not re-render when you change the value remove/change the interval.

Function not pointing to the updated value of state variable using hooks

I am currently refactoring a react app from setState to hooks. I can't understand why the state variables aren't changed. Here is an example:
import React, { useState, useEffect } from 'react';
function Hook() {
const [num, setNum] = useState(1);
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, []);
const logNum = () => {
console.log(num);
}
const handleToggle = () => {
if (num == 1) {
console.log('setting num to 2');
setNum(2);
} else {
console.log('setting num to 1');
setNum(1);
}
}
return (
<div>
<button onClick={handleToggle}>TOGGLE BOOL</button>
</div>
);
}
export default Hook;
When i click the button, I was expecting the output to be something like:
// 1
// setting num to 2
// 2
// setting num to 1
// 1
But the output look like this:
Why is the updated num variable not logged?
Shouldn't the logNum() function always point to the current value of the state?
That's why effect dependencies have to be exhaustive. Don't lie about dependencies.
logNum closures over num, so on every rerender there is a new num variable containing the new value, and a new logNum function logging that value. Your effect however gets initialized only once, thus it only knows the first logNum. Therefore, you have to add logNum as a dependency, so that the effect gets updated whenever num and thus logNum changes:
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, [logNum]);
You'll notice that your effect does not correctly clean up, you should add a removeEventListener too.
return () => window.removeEventListener("mousemove", logNum);
Now if you debug this piece of code, you'll notice that the effect triggers on every rerender. That is because a new logNum function gets created on every rerender, no matter wether num changes or not. To prevent that, you can use useCallback to make the logNum reference stable:
const logNum = useCallback(() => console.log(num), [num]);
An alternative to all of this would be to use a reference to the current state:
const actualNum = useRef(num);
// that works no matter when and how this is executed
console.log(actualNum.current);

Categories

Resources