React hooks useInterval reset after button click - reactjs

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)

Related

Refactor a custom hook to be called inside a method

I created this hook that is responsible to do something after an amount of time:
const useTime = (callback, timeout = 1000) => {
useEffect(() => {
const timer = setTimeout(() => {
callback()
}, timeout);
return () => clearTimeout(timer);
}, []);
}
The hook is working, but i can not call it inside a method like:
{
clear: () => {
useTime(() => console.log('close'), 6000 )
},
... this is happen because of hooks rules. Question: How to refactor the hook to be able to call it inside a method or a function?
You probably need to do like this -
function useTime(cb, timeout = 100) {
const timer = setTimeout(() => {
cb();
}, timeout);
return () => clearTimeout(timer);
}
function anotherMethod() {
const cancel = useTime(runJob, 1000);
// if you wanna cancel the timer, just call the cancel function
cancel();
}
You can try something around this:
const useTime = () => {
const timer = useRef();
const fn = useCallback((callback, timeout = 1000) => {
timer.current = setTimeout(() => {
callback()
}, timeout);
}, []);
useEffect(() => {
return () => clearTimeout(timer.current);
}, []);
return fn;
}
const delayedFn = useTime();
clear: () => {
delayedFn(() => console.log('close'), 6000)
},

Can't perform a React state update on an unmounted component. This is a no-op

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;

The unsubscribe () in Rxjs method does not work on React js

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);
}, []);

React : Access multiple states in setInterval() in useEffect()

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]);

setInterval can't be stopped by clearInterval when use setState in React

const handleClick = () => {
setState({ ...state, on: !state.on });
let tog = state.on;
console.log("first" + tog);
const interval = setInterval(() => {
if (tog) {
console.log(tog);
} else clearInterval(interval);
}, 1000);
};
enter image description here
this one will not be able to stop even the tog is false;
however if I don't use state, change to a variable it will not happen,
it is so weird for me, I need some help;
let flag = true;
const handleClick = () => {
flag = !flag;
console.log("first" + flag);
const interval = setInterval(()=>{
if(flag){
console.log(flag);
}else(clearInterval(interval))
},1000)
};
React will create new handleClick on every re-renders, and there will be different setIntervals,
const intvl = useRef(null);
const handleClick = () => {
intvl.current = setInterval(() => { //now interval is not changing on every fn recreation
.....
clearInterval(intvl.current);
}
check this one
You should use an Effect to handle intervals, something like this:
useEffect(() => {
let intervalId
if (state.on) {
intervalId = setInterval(() => {
console.log(state.on)
}, 1000)
return () => clearInterval(intervalId)
}
}, [state.on])
const handleClick = () => {
setState({ ...state, on: !state.on });
};
working code

Resources