clearInterval() doesn't clear interval in React - javascript

I want to increment the number of users after each 200ms till 5000 with the below code. But it doesn't clear the interval when the number of users greater than 5000.
const Cards = () => {
const [users, setUsers] = useState(40);
useEffect(() => {
const setIntervalUsers = setInterval(() => {
setUsers((prevUsers) => prevUsers = prevUsers + 100)
}, 200);
if (users >= 5000) {
console.log('ok');
clearInterval(setIntervalUsers)
}
}, []);
return (<div>number of users {users} </div>)}

I would suggest you to return a clean up function so you don't register the interval twice in case you are in StrictMode with React 18, and also to remove it from the memory when the component gets unmounted.
Also use a ref set with useRef and a separate useEffect that would watch changes in users and clear the interval there. Like so:
import { useEffect, useRef, useState } from "react";
const Cards = () => {
const [users, setUsers] = useState(40);
const intervalRef = useRef();
useEffect(() => {
if (users >= 5000) {
console.log("ok");
clearInterval(intervalRef.current);
}
}, [users]);
useEffect(() => {
intervalRef.current = setInterval(() => {
setUsers((prevUsers) => (prevUsers = prevUsers + 100));
}, 200);
return () => clearInterval(intervalRef.current);
}, []);
return <div>number of users {users} </div>;
};

This doesnt work because:
you never call the useEffect again to check if the condition is met
the interval ref is lost
I made a working sample of your code here : https://codepen.io/aSH-uncover/pen/wvmYdNy
Addintionnaly you should clean the interval when the component is destroyed by returning the cleanInterval call in the hook that created the inteerval
const Card = ({ step }) => {
const intervals = useRef({})
const [users, setUsers] = useState(40)
useEffect(() => {
intervals.users = setInterval(() => {
setUsers((prevUsers) => prevUsers = prevUsers + step)
}, 200)
return () => clearInterval(intervals.users)
}, [])
useEffect(() => {
if (users >= 5000) {
clearInterval(intervals.users)
}
}, [users])
return (<div>number of users {users} </div>)
}

I came up with this. You can try it out. Although there are many ways suggested above
const [users, setUsers] = useState(40);
const [max_user, setMaxUser] = useState(true);
let setIntervalUsers: any;
let sprevUsers = 0;
useEffect(() => {
if (max_user) {
setIntervalUsers = setInterval(() => {
sprevUsers += 100;
if (sprevUsers >= 5000) {
setMaxUser(false);
clearInterval(setIntervalUsers);
} else {
setUsers(sprevUsers);
}
}, 200);
}
}, []);

The way how you check for your condition users >= 5000 is not working because users is not listed as a dependency in your useEffect hook. Therefore the hook only runs once but doesnt run again when users change. Because of that you only check for 40 >= 5000 once at the beginning.
An easier way to handle that is without a setInterval way.
export const Cards = () => {
const [users, setUsers] = useState(40);
useEffect(() => {
// your break condition
if (users >= 5000) return;
const increment = async () => {
// your interval
await new Promise((resolve) => setTimeout(resolve, 200));
setUsers((prevState) => prevState + 100);
}
// call your callback
increment();
// make the function run when users change.
}, [users]);
return <p>current number of users {users}</p>
}

Related

React: ClearInterval and Immediately Start Again

I have a component that sets off a timer which updates and makes an axios request every 30 seconds. It uses a useRef which is set to update every 30 seconds as soon as a function handleStart is fired.
const countRef = useRef(null);
const lastUpdatedRef = useRef(null);
const [lastUpdated, setLastUpdated] = useState(Date.now())
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
Now I have a useEffect that runs a calculate function every 30 seconds whenever lastUpdated is triggered as a dependency:
const firstCalculate = useRef(true);
useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
This updates the calculate function every 30 seconds (00:30, 01:00, 01:30 etc.) as per lastUpdatedRef. However, I want the timer to restart from when lastUpdated state has been modified elsewhere (e.g. if lastUpdated was modified at 00:08, the next updated will be 00:38, 01:08, 01:38 etc.). Is there a way to do this?
Basically it sounds like you just need another handler to clear and restart the 30 second interval updating the lastUpdated state.
Example:
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
}
Full example:
const calculateModel = () => console.log("calculateModel");
export default function App() {
const countRef = React.useRef(null);
const lastUpdatedRef = React.useRef(null);
const [lastUpdated, setLastUpdated] = React.useState(Date.now());
const [timer, setTimer] = React.useState(0);
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const firstCalculate = React.useRef(true);
React.useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>Timer: {timer}</div>
<button type="button" onClick={handleStart}>
Start
</button>
<button type="button" onClick={handleOther}>
Other
</button>
</div>
);
}
Don't forget to clear any running intervals when the component unmounts!
React.useEffect(() => {
return () => {
clearInterval(countRef.current);
clearInterval(lastUpdatedRef.current);
};
}, []);

Updated state value is not reflected inside setInterval() in React

I have the following:
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // always says "false"
}, 1000);
};
Elsewhere in the code while this timer is running I'm updating the value of isPaused:
setIsPaused(true);
But this isn't reflected in the console log, it always logs false. Is there a fix to this?
The myTimer.current never changed which means isPaused is always false inside the function.
You need to make use of useEffect to update myTimer.current every time isPaused is updated.
useEffect(() => {
function startTimer() {
myTimer.current = setInterval(() => {
console.log(isPaused);
}, 1000);
};
startTimer();
return () => clearInterval(myTimer.current); // cleanup
}, [isPaused]);
You can do something like this,
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // now updates
}, 1000);
};
useEffect(() => {
startTimer();
return () => myTimer.current != null && clearInterval(myTimer.current);
}, [isPaused]);
return (
<div>
<b>isPaused: {isPaused ? "T" : "F"}</b>
<button onClick={() => setIsPaused(!isPaused)}>Toggle</button>
</div>
);
Use Others function
use useInterval from 30secondsofcode
const Timer = props => {
const [seconds, setSeconds] = React.useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <p>{seconds}</p>;
};
ReactDOM.render(<Timer />, document.getElementById('root'));
Or, use react-useInterval package
function Counter() {
let [count, setCount] = useState(0);
const increaseCount = amount => {
setCount(count + amount);
};
useInterval(increaseCount, 1000, 5);
return <h1>{count}</h1>;
}

React Hook useEffect has a missing dependency: 'refreshSells'. Either include it or remove the dependency array

i want to add setInterval to be able to get new data from database without needing to refresh the page so i used useEffect,setInterval,useState to solve it,put intial state {refresh : false, refreshSells: null}
and there is switch when it on refresh = true and refreshSells= setinterval() but i got annoying warning
React Hook useEffect has a missing dependency: 'refreshSells'. Either include it or remove the dependency array
and if i add refreshSells it will be unstoppable loop
const Sells = () => {
const [allSells,setAllSells] = useState([])
const [refresh,setRefresh] = useState(false)
const [refreshSells , setRefreshSells] = useState(null)
const [hidden,setHidden] = useState(true)
useEffect(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
.catch(() => {
alert('something went wrong,ask omar')
})
},[])
useEffect(() => {
if(refresh){
setRefreshSells(setInterval(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
}, 60000));
}
else{
clearInterval(refreshSells)
}
return () => clearInterval(refreshSells)
},[refresh])
setRefreshSells updates internal state and doesn't change refreshSells during current render. So return () => clearInterval(refreshSells) will try to clear the wrong interval.
You should use useRef hook for your interval:
const refreshSellsRef = useRef(null);
...
useEffect(() => {
if(refresh){
refreshSellsRef.current = setInterval(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
}, 60000);
return () => clearInterval(refreshSellsRef.current);
}
},[refresh])
Also note that return () => clearInterval(refreshSellsRef.current) will be called on unmount and when refresh changes. So you don't need else {clearInterval(...)}
If your business logic allows to separate the 2 effects (automatic refresh every 60s + manual refresh after clicking some button), that would simplify the code for each independent effect:
useEffect(() => {
const interval = setInterval(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
}, 60000)
return () => clearInterval(interval)
}, [])
useEffect(() => {
if (refresh) {
setRefresh(false)
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
};
}, [refresh])
It looks like you forgot to setRefresh(false) after triggering the refresh, but I am not sure why you needed refreshSells in the first place...
In the second useEffect you're updating the state refreshSells where useEffect expecting useCallback ref as a dependency. If you add refreshSells as a dependency to the useEffect then you may endup into memory leak issue.
So I suggest you try the below code which will solve your problem. by this you can also eliminate refreshSells
useEffect(() => {
let interval;
if (refresh) {
interval = setInterval(() => {
Axios.get('/sells')
.then(({ data }) => {
setAllSells(data.sells)
});
}, 4000);
}
return () => clearInterval(interval);
}, [refresh]);

cant append array(state element) with setinterval() in react

const [timer,setTimer] = useState()
const [number, setNumber] = useState()
const [list, setlist] = useState([])
const numberChange = (number)=>{
setNumber(number)
if (!(list.find(item=>item===number))){
setlist([...list,number])}
}
const randomNumber=()=> 1+Math.floor(Math.random()*90)
const randNumberChange=()=>{
let randNumber = randomNumber()
if (list.find(item=>item===randNumber))
randNumberChange()
else
numberChange(randNumber)
}
const startTimer = () => {
setTimer(setInterval(()=>{
randNumberChange()
}, 5000))
}
const stopTimer=()=>{
clearInterval(timer)
}
The list is always rendering only one item and not appending it.
When randNumberChange is called separately then the list gets appended but not with setInterval.
When startTimer funcion is executed is stopped with stopTimer and then started again it appends second item then stop and it repeats
Change setlist([...list,number])} to setlist((prevState) => [...prevState, number]). React set state is async in nature. So to get the correct list value from the state you would need to get the value from previous state. Doc
Suggestion: that instead of setting timer in state, you can start the interval in useEffect.
Also in numberChange function, you should get the list from previous state and then append the new number in that. This will make sure that the list value is updated before adding new number.
import React, { Component, useState } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import "./style.css";
const Test = () => {
const [number, setNumber] = useState(null);
const [list, setlist] = useState([]);
const numberChange = number => {
setNumber(number);
if (!list.find(item => item === number)) {
setlist((prevState) => [...prevState, number]);// instead of directly using list value, get it from previous state
}
};
const randomNumber = () => 1 + Math.floor(Math.random() * 90);
const randNumberChange = () => {
console.log("here");
let randNumber = randomNumber();
if (list.find(item => item === randNumber)) randNumberChange();
else numberChange(randNumber);
};
const startTimer = () => {
return setInterval(() => { randNumberChange(); }, 5000);
}
const stopTimer = (timer) => {
clearInterval(timer)
}
React.useEffect(() => {
const timer = startTimer();
return ()=> stopTimer(timer);
}, []);
console.log(list);
return <div>{number}</div>;
};
You'll need to use useEffect hook:
useEffect(() => {
// You don't need timer state, we'll clear this later
const interval = setInterval(() => {
randNumberChange()
},5000)
return () => { // clear up
clearInterval(interval)
}
},[])
use useEffect with setTimeout it is work as setInterval
useEffect(() => {
setTimeout(() => setList([...list, newValue]), 2000)
}, [list])

Update state from axios call in useEffect hook

I'm new to react and I've just started learning about hooks and context.
I am getting some data from an API with the following code:
const getScreen = async uuid => {
const res = await axios.get(
`${url}/api/screen/${uuid}`
);
dispatch({
type: GET_SCREEN,
payload: res.data
});
};
Which goes on to use a reducer.
case GET_SCREEN:
return {
...state,
screen: action.payload,
};
In Screen.js, I am calling getScreen and sending the UUID to show the exact screen. Works great. The issue I am having is when I am trying to fetch the API (every 3 seconds for testing) and update the state of nodeupdated based on what it retrieves from the API. The issue is, screen.data is always undefined (due to it being asynchronous?)
import React, {
useState,
useEffect,
useContext,
} from 'react';
import SignageContext from '../../context/signage/signageContext';
const Screen = ({ match }) => {
const signageContext = useContext(SignageContext);
const { getScreen, screen } = signageContext;
const [nodeupdated, setNodeupdated] = useState('null');
const foo = async () => {
getScreen(match.params.id);
setTimeout(foo, 3000);
};
useEffect(() => {
foo();
setNodeupdated(screen.data)
}, []);
If I remove the [] is does actually get the data from the api ... but in an infinate loop.
The thing is this seemed to work perfectly before I converted it to hooks:
componentDidMount() {
// Get screen from UUID in url
this.props.getScreen(this.props.match.params.id);
// Get screen every 30.5 seconds
setInterval(() => {
this.props.getScreen(this.props.match.params.id);
this.setState({
nodeUpdated: this.props.screen.data.changed
});
}, 3000);
}
Use a custom hook like useInterval
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
Then in your component
useInterval(() => {
setCount(count + 1);
}, delay);
Dan Abramov has a great blog post about this
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
You can use some thing like this. Just replace the console.log with your API request.
useEffect(() => {
const interval = setInterval(() => {
console.log("Making request");
}, 3000);
return () => clearInterval(interval);
}, []);
Alternatively, Replace foo, useEffect and add requestId
const [requestId, setRequestId] = useState(0);
const foo = async () => {
getScreen(match.params.id);
setTimeout(setRequestId(requestId+1), 3000);
};
useEffect(() => {
foo();
setNodeupdated(screen.data)
}, [requestId]);

Categories