I was trying to build a simple counter application, the app starts counting (+1/sec) when I click the start button and stops when I click the stop button.
I came up with 2 different solutions, one using setTimeout and the other using a for loop with delay. both these solutions work for incrementing the number. Are both of these valid react ways for creating a counter? is there a better way to do this?
When stopping the app I can stop it by changing the reference variable halt.current = true but changing the state setStop(true) does nothing, why is that?
function App() {
const [get, set] = useState(0);
const [stop, setStop] = useState(false);
const halt = useRef(false);
const loopfn = async () => {
halt.current = false;
setStop(false);
while (true) {
if (halt.current || stop) break;
await new Promise((res) => setTimeout(res, 1000));
set((prev: number) => prev + 1);
}
};
const timeoutloopfn = () => {
halt.current = false;
setStop(false);
setTimeout(() => {
if (halt.current || stop) return;
set((prev: number) => prev + 1);
timeoutloopfn();
}, 1000);
};
const stoploopref = () => {
halt.current = true;
};
const stoploopst = () => {
setStop((prev: boolean) => true);
};
return (
<div>
<button onClick={loopfn}>for-loop increment</button>
<button onClick={timeoutloopfn}>timeout increment</button>
<button onClick={stoploopref}>stop using ref</button>
<button onClick={stoploopst}>stop using state</button>
<button onClick={() => set(0)}>reset</button>
<p>{get}</p>
</div>
);
}
You may consider using the setInterval function instead, storing its id in a state and clearing it when stop is set to true:
function App() {
const [get, set] = useState(0);
const [stop, setStop] = useState(false);
const [intervalId, setIntervalId] = useState(-1);
const halt = useRef(false);
useEffect(() => {
// Stop the loop
if (stop && intervalId !== -1) {
clearInterval(intervalId)
setIntervalId(-1)
}
}, [stop])
const timeoutloopfn = () => {
halt.current = false;
setStop(false);
const newIntervalId = setInterval(() => {
set((prev: number) => prev + 1);
}, 1000);
setIntervalId(newIntervalId)
};
I would totally recommend useEffect for every function that requires timing.
import React, { useRef, useEffect, useState } from "react";
export default function App() {
const [number, setNumber] = useState(100);
let intervalRef = useRef();
const decreaseNum = () => setNumber(prev => prev - 1);
useEffect(() => {
intervalRef.current = setInterval(decreaseNum, 1000);
return () => clearInterval(intervalRef.current);
}, []);
return <div>{number}</div>;
}
Related
I am creating a timer and i want the timer to start ticking backwards based on the input value provided. I am confused on how to set the initialState in the useState hook instead of taking the default value as 0
Timer.js
const Timer = () => {
const [input,setInput] = useState();
const inputHandler = (e) => {
setInput(e.target.value);
}
const [time,setTime] = useState(0);
let timer;
useEffect(()=>{
timer = setInterval(() => {
if(time > 0){
setTime(time-1);
}
},1000)
return () => clearInterval(timer);
},[])
return (
<>
<h1>Timer</h1>
<input value = {input} onChange = {inputHandler}/>
<h1>{time}</h1>
</>
)
}
App.js
import './App.css';
import Timer from './components/Timer';
function App() {
return (
<div className="App">
<Timer />
</div>
);
}
export default App;
You can specify the type of your input
and initialize it like that. Suppose it's a number and the initial value is 1:
const [input, setInput] = useState<number>(1);
const Timer = () => {
const [input, setInput] = useState(0)
const [time, setTime] = useState(0)
const inputHandler = (e) => {
setTime(e.target.value)
setInput(e.target.value)
}
let timer
useEffect(() => {
timer = setInterval(() => {
if (time > 0) {
setTime(time - 1)
setInput(time - 1)
console.log('1')
}
}, 1000)
return () => clearInterval(timer)
}, [input])
return (
<>
<h1>Timer</h1>
<input defaultValue={input} onChange={inputHandler} />
<h1>{time}</h1>
</>
)
}
Use this code bro 😎
That's the warning in the console,
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Here is my code
const [index, setIndex] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refContainer: any = useRef();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigation = useNavigation();
useEffect(() => {
refContainer.current.scrollToIndex({animated: true, index});
}, [index]);
const theNext = (index: number) => {
if (index < departments.length - 1) {
setIndex(index + 1);
setSelectedIndex(index + 1);
}
};
setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
const onRefresh = () => {
if (refreshing === false) {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}
};
What should I do to make clean up?
I tried to do many things but the warning doesn't disappear
setTimeout need to use in useEffect instead. And add clear timeout in return
useEffect(() => {
const timeOut = setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
return () => {
if (timeOut) {
clearTimeout(timeOut);
}
};
}, []);
Here is a simple solution. first of all, you have to remove all the timers like this.
useEffect(() => {
return () => remover timers here ;
},[])
and put this
import React, { useEffect,useRef, useState } from 'react'
const Example = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
isScreenMounted.current = true
return () => isScreenMounted.current = false
},[])
const somefunction = () => {
// put this statement before every state update and you will never get that earrning
if(!isScreenMounted.current) return;
/// put here state update function
}
return null
}
export default Example;
I have hook useInterval which download data every 10 seconds automaticaly, however I have also button which can manually download data in every moment. I'm struggling to restart interval timer when I click button. So basically if interval counts to 5, but I click button meantime, interval should restart and starts counting to 10 again before downloading data
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
export default useInterval;
APP PART:
useInterval(() => {
getMessage();
}, 10000)
const getMessage = async () => {
setProcessing(true)
try {
const res = await fetch('url')
const response = await res.json();
setRecievedData(response)
}
catch (e) {
console.log(e)
}
finally {
setProcessing(false)
}
}
const getMessageManually = () => {
getMessage()
RESTART INTERVAL
}
You can add a reset function in the hook and return that function. The reset function should clear the existing interval and start a new one.
Here is the code for the hook which can be reset and stopped.
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
const intervalRef = useRef(null);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(savedCallback.current, delay);
intervalRef.current = id;
return () => clearInterval(id);
}
}, [delay]);
useEffect(()=>{
// clear interval on when component gets removed to avoid memory leaks
return () => clearInterval(intervalRef.current);
},[])
const reset = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
intervalRef.current = setInterval(savedCallback.current,delay)
}
});
const stop = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
}
})
return {
reset,
stop
};
};
// usage
const {reset,stop} = useInterval(()=>{},10000);
reset();
stop();
You should add a reset function as returning a value from the hook.
I also fixed few issues and added an unmount handler:
// Usage
const resetInterval = useInterval(() => ..., DELAY);
resetInterval();
// Implementation
const useInterval = (callback, delay) => {
const savedCallbackRef = useRef(callback);
const intervalIdRef = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// handle tick
useEffect(() => {
const tick = () => {
savedCallback.current();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
const resetInterval = useCallback(() => {
clearInterval(intervalIdRef.current);
intervalIdRef.current = setInterval(savedCallback.current, delay)
}, [delay]);
return resetInterval;
};
Another solution is to remove the ref on the callback making the hook restart the count on every change to the callback
so updating the above solution
// Implementation
const useInterval = (callback, delay) => {
const intervalIdRef = useRef();
// handle tick
useEffect(() => {
const tick = () => {
callback();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
};
And then you can use it like this
const [counter, setCounter] = useState[0]
const onTimerFinish = useCallback(() => {
setCounter(counter + 1)
// setCounter will reset the interval
}, [counter])
useResetInterval(() => {
onTimerFinish()
}, 5000)
In the file timer.js I am exporting this variable initTimer (create stream)
export const initTimer=new Observable((observer)=>{
interval(1000)
.subscribe(val=>{
observer.next(val)})
})
in App
const [sec, setSec] = useState(0);
const [status, setStatus] = useState("start" | "stop" | "wait");
const subscribe=()=>{
return initTimer.subscribe({next(x){
setSec(x=>x+1000)
}})}
useEffect(() => {
if(status==="start"){
subscribe()
}
if(status==="stop"){
subscribe().unsubscribe()
}
}, [status]);
const start = React.useCallback(() => {
setStatus("start");
}, []);
const stop = React.useCallback(() => {
setStatus("stop");
setSec(0);
}, []);
return (
<div>
<span> {new Date(sec).toISOString().slice(11, 19)}</span>
<button className="start-button" onClick={start}>
Start
</button>
<button className="stop-button" onClick={stop}>
Stop
</button>
</div>
);
}
When "start" is triggered, I subscribe to the timer, call the method
"next" and add the resulting result to state. But when the "stop" condition is triggered, I must unsubscribe and the timer must stop counting, but when I unsubscribe the timer is reset and the countdown begins. How do I stop the timer?
You need to store your subscription so that you can use it to unsubscribe later.
You can do this using userRef:
const [sec, setSec] = useState(0);
const [status, setStatus] = useState("wait");
const sub = useRef();
useEffect(() => {
// Subscribe and store subscription
if (status === "start") {
sub.current = initTimer.subscribe({
next(x) {
setSec(x => x + 1000);
}
});
}
// Unsubscribe
if (status === "stop") {
if (sub.current) {
sub.current.unsubscribe();
}
}
// Return cleanup function to unsubscribe when component unmounts
return () => {
if (sub.current) {
sub.current.unsubscribe();
}
}
}, [status]);
const start = React.useCallback(() => {
setStatus("start");
}, []);
const stop = React.useCallback(() => {
setStatus("stop");
setSec(0);
}, []);
I have this code which updates the state count every 1 seconds.
How can I access the value of the state object in setInterval() ?
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
export default function Timer({objectId}) {
const [object, setObject] = useState({increment: 1});
const [count, setCount] = useState(0);
useEffect(() => {
callAPI(); // update state.object.increment
const timer = setInterval(() => {
setCount(count => count + object.increment); // update state.count with state.object.increment
}, 1000);
return () => clearTimeout(timer); // Help to eliminate the potential of stacking timeouts and causing an error
}, [objectId]); // ensure this calls only once the API
const callAPI = async () => {
return await axios
.get(`/get-object/${objectId}`)
.then(response => {
setObject(response.data);
})
};
return (
<div>{count}</div>
)
}
The only solution I found is this :
// Only this seems to work
const timer = setInterval(() => {
let increment = null;
setObject(object => { increment=object.increment; return object;}); // huge hack to get the value of the 2nd state
setCount(count => count + increment);
}, 1000);
In your interval you have closures on object.increment, you should use useRef instead:
const objectRef = useRef({ increment: 1 });
useEffect(() => {
const callAPI = async () => {
return await axios.get(`/get-object/${objectId}`).then((response) => {
objectRef.current.increment = response.data;
});
};
callAPI();
const timer = setInterval(() => {
setCount((count) => count + objectRef.current);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [objectId]);