Why is clearTimeout not clearing the timeout in this react component? - javascript

I am attempting to clear a former timeout before initiating a new timeout, because I want messages to display for 4 seconds and disappear UNLESS a new message pops up before the 4 seconds is up. The Problem: Old timeouts are clearing the current message, so clearTimeout() is not working in this component, in this scenario:
let t; // "t" for "timer"
const [message, updateMessage] = useState('This message is to appear for 4 seconds. Unless a new message replaces it.');
function clearLogger() {
clearTimeout(t);
t = setTimeout(() => {
console.log('wiping message');
updateMessage('');
}, 4000);
}
function initMessage(msg) {
updateMessage(msg);
clearLogger();
}
The funny thing is that this works:
function clearLogger() {
t = setTimeout(() => {
console.log('wiping message');
updateMessage('');
}, 4000);
clearTimeout(t);
}
...but obviously defeats the purpose, since it just immediately obliterates the timeout.
In practice, I should be able to trigger initMessage() every two seconds and never see, "wiping message' logged to the console.

The issue is that on every render the value of t is reset to null. Once you call updateMessage, it will trigger a re-render and will lose it's value. Any variables inside a functional react component get reset on every render (just like inside the render function of a class-based component). You need to save away the value of t using setState if you want to preserve the reference so you can call clearInterval.
However, another way to solve it is to promisify setTimeout. By making it a promise, you remove needing t because it won't resolve until setTimeout finishes. Once it's finished, you can updateMessage('') to reset message. This allows avoids the issue that you're having with your reference to t.
clearLogger = () => {
return new Promise(resolve => setTimeout(() => updateMessage(''), resolve), 5000));
};
const initMessage = async (msg) => {
updateMessage(msg);
await clearLogger();
}

I solved this with useEffect. You want to clear the timeout in the return function
const [message, updateMessage] = useState(msg);
useEffect(() => {
const t = setTimeout(() => {
console.log('wiping message');
updateMessage('');
}, 4000);
return () => {
clearTimeout(t)
}
}, [message])
function initMessage(msg) {
updateMessage(msg);
}

Try execute set timeout after clearTimeout() completes
clearTimeout(someVariable, function() {
t = setTimeout(() => {
console.log('wiping message');
updateMessage('');
}, 4000);
});
function clearTimeout(param, callback) {
//`enter code here`do stuff
}
Or you can use .then() as well.
clearTimeout(param).then(function(){
t = setTimeout(() => {
console.log('wiping message');
updateMessage('');
}, 4000);
});

Related

React - State keeps same value even tho it is updated. (Inside function)

so I have a function that runs every 5 seconds. Inside this function, I check if the state is null, to then set a value to it. The problem is that, every time the function run, it detects the state as null, even tho it is not null.
My code:
const [activeChat, setActiveChat] = useState(null)
const loadChats = async () => {
await api.get('/v1/chat/chats')
.then((res) => {
if (activeChat === null) {
if (res.data.chats.length > 0) {
setActiveChat(res.data.chats[0])
}
}
setChats(res.data.chats)
setLoading(false)
})
.catch((err) => {
setLoading(false)
})
}
useEffect(() => {
loadChats()
let interval = setInterval(() => {
loadChats()
}, 5000);
return () => clearInterval(interval)
}, [])
the activeChat should be only set on the first load, if its not set yet, but it keeps detecting as null every time the function runs. Why does it keep detecting as null?
Obs: As I said, the state is really being set, as expected, so the problem is not with the response or something, i don't know what is happening..
Inside setInterval or setTimeout, the state will NOT be changed even you have changed it inside the setInterval function. Try to create a timer using setInterval and you can see the state does not change inside it.
const [timer, setTimer] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(timer); // remains the same (0) forever
setTimer(timer + 1);
}, 1000);
}, []);
useEffect(() => {
console.log(timer); // this one should only change from 0 to 1, because timer always being set as 0 + 1;
}, [timer]);
You can create a ref for the chat object only for updating, and a state only for the chat display. Use the ref to keep track of the active chat, while using the state for UI display.

Using setTimeout function not working in React

am trying to use the setTimeout function if the user clicks on the button, I want it to display successfully for just 3sec, it displaying but it's not executing the 3sec time given. what am I doing wrong?
Here’s my code
const [message, setMessage] = useState('')
function handleSubmit (e) {
e.preventDefault()
emailjs.sendForm(process.env.SERVICE_ID,process.env.TEMPLATE_ID, form.current,process.env.PUBLIC_KEY)
.then(function(response) {
return setTimeout(setMessage("successFully sent"), 3000)
}, function(err) {
console.log('FAILED...', err);
});
}
setTimeout(func,n) executes func after n milliseconds.
If you want the message to be displayed during 3s,
Set the message immediately
Clear the message after 3s.
Inside your then callback,
setMessage('successfully sent');
setTimeout(()=>{ // may start timer before message is set.
setMessage('')
}, 3000);
However due the asynchronous nature of state updates, the setTimeout may be "set" before the success message, resulting in the message for being shown for, example 2.993 seconds.
A more precise solution would be:
handleSubmit=(evt)=> { // arrow functions preserve 'this' of component
// ....
.then((response)=> {
this.setState({message: 'Successfully sent'}, ()=> {
setTimeout(()=> { // start timer after the message is set
this.setState({message: ''});
}, 3000);
});
})
// ...
}
Here the 2nd callback argument of setState is run, after message is set.
you need to use a anonymous function because right now you're calling the function when using setTimeout
const [message, setMessage] = useState('')
function handleSubmit (e) {
e.preventDefault()
emailjs.sendForm(process.env.SERVICE_ID,process.env.TEMPLATE_ID, form.current,process.env.PUBLIC_KEY)
.then(function(response) {
return setTimeout(() => setMessage("successFully sent"), 3000)
}, function(err) {
console.log('FAILED...', err);
});
}

React stop timeout when useEffect prop changes

I'm building and React application, where I have to save on what page of online display document user currently is, but there is a problem, that if users scroll throughout the document, it saves all pages. So we want to use some kind of timer function, that would only trigger if prop page hasn't changed in 30 seconds for example. Here is my code, it invokes later, but still for all pages through the scroll.
useEffect(
async () => {
let timeout;
if (scriptInfo && authData && numPages) {
setTimeout(async () => {
const res = await postScriptAnalyticsData({
script_id: scriptInfo._id,
user_id: authData.user.user_id,
page: page,
full_page: numPages
});
}, 10000);
}
return () => {
clearTimeout(timeout);
};
},
[ scriptInfo, page, authData, numPages ]
);
Your useEffect() callback function should not be async. The useEffect hook should return a function when a value is returned, but if your callback is async, then it will implicitly return a Promise. Remove the async from your useEffect callback, as this isn't needed as you're not using await directly within the function. Also, assign the timeout to the return value of setTimeout() so you can clear it:
useEffect(() => { // can't be `async`, so remove it
let timeout;
if (scriptInfo && authData && numPages) {
timeout = setTimeout(async () => { // assign `timeout`
const res = await postScriptAnalyticsData({
script_id: scriptInfo._id,
user_id: authData.user.user_id,
page: page,
full_page: numPages
});
}, 10000);
}
return () => {
clearTimeout(timeout);
};
},
[scriptInfo, page, authData, numPages]
);
Declare the setTimeout() ... , out of the components cause or if it is not using hooks
And keep it in a variable
Ex:
const timerCustom = setTimeout(...)
// Component
useEffect (() => {
...
clearTimeout(timerCustom);
...
})
and just clear it when your page props changed

How to wait my custom command finish, then executing remaining Cypress commands

I'm getting trouble with Cypress asynchronous mechanism. I have a custom command that is placed in this file
class HeaderPage {
shopLink = 'a[href="/angularpractice/shop"]'
homeLink = ''
navigateToShopPage() {
cy.get(this.shopLink).click()
}
sshToServer() {
setTimeout(() => {
console.log('Connecting')
}, 5000)
console.log('Connected')
}
}
export default HeaderPage
The function sshToServer is simulated to pause 5000ms. So I want Cypress remaining test will be hold and wait for this function completed. How can I do that? Thanks in advance.
import HeaderPage from "../support/pageObjects/HeaderPage"
describe('Vefiry Alert and Confirm box', () => {
const headerPage = new HeaderPage()
it('Access home page', () => {
cy.visit(Cypress.env('url') + 'AutomationPractice/')
});
it('SSH to server', () => {
headerPage.sshToServer()
});
it('Verify content of Alert', () => {
cy.get('#alertbtn').click()
cy.on('window:alert', (alert) => {
expect(alert).to.equal('Hello , share this practice page and share your knowledge')
})
});
You can issue a new cy.wrap command with a value null and calling your async function in the .then function. Cypress will resolve that async function automatically and then move on to the next test.
First, you should convert your sshToServer method to an async(promise) function:
sshToServer() {
console.log('Connecting');
return new Promise((resolve) => {
setTimeout(() => {
console.log('Connected');
resolve();
}, 5000);
});
}
Then, in your spec:
it('SSH to server', { defaultCommandTimeout: 5000 }, () => {
cy.wrap(null).then(() => headerPage.sshToServer());
});
Note that I have also used a bigger for the spec defaultCommandTimeout since the default timeout is 4 seconds which is shorter than your sshToServer method and would cause the test to fail if not using a bigger timeout.
Sorry for late answer but i think this is the easiest way to do it
cy.wait(milliseconds)

Polling API every x seconds with react

I have to monitoring some data update info on the screen each one or two seconds.
The way I figured that was using this implementation:
componentDidMount() {
this.timer = setInterval(()=> this.getItems(), 1000);
}
componentWillUnmount() {
this.timer = null;
}
getItems() {
fetch(this.getEndpoint('api url endpoint'))
.then(result => result.json())
.then(result => this.setState({ items: result }));
}
Is this the correct approach?
Well, since you have only an API and don't have control over it in order to change it to use sockets, the only way you have is to poll.
As per your polling is concerned, you're doing the decent approach. But there is one catch in your code above.
componentDidMount() {
this.timer = setInterval(()=> this.getItems(), 1000);
}
componentWillUnmount() {
this.timer = null; // here...
}
getItems() {
fetch(this.getEndpoint('api url endpoint'))
.then(result => result.json())
.then(result => this.setState({ items: result }));
}
The issue here is that once your component unmounts, though the reference to interval that you stored in this.timer is set to null, it is not stopped yet. The interval will keep invoking the handler even after your component has been unmounted and will try to setState in a component which no longer exists.
To handle it properly use clearInterval(this.timer) first and then set this.timer = null.
Also, the fetch call is asynchronous, which might cause the same issue. Make it cancelable and cancel if any fetch is incomplete.
I hope this helps.
Although an old question it was the top result when I searched for React Polling and didn't have an answer that worked with Hooks.
// utils.js
import React, { useState, useEffect, useRef } from 'react';
export const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
You can then just import and use.
// MyPage.js
import useInterval from '../utils';
const MyPage = () => {
useInterval(() => {
// put your interval code here.
}, 1000 * 10);
return <div>my page content</div>;
}
You could use a combination of setTimeout and clearTimeout.
setInterval would fire the API call every 'x' seconds irrespective whether the previous call succeeded or failed. This can eat into your browser memory and degrade performance over time. Moreover, if the server is down, setInterval would continue to bombard the server not knowing its down status.
Whereas,
You could do a recursion using setTimeout. Fire a subsequent API call, only if the previous API call succeed. If previous call has failed, clear the timeout and do not fire any further calls. if required, alert the user on failure. Let the user refresh the page to restart this process.
Here is an example code:
let apiTimeout = setTimeout(fetchAPIData, 1000);
function fetchAPIData(){
fetch('API_END_POINT')
.then(res => {
if(res.statusCode == 200){
// Process the response and update the view.
// Recreate a setTimeout API call which will be fired after 1 second.
apiTimeout = setTimeout(fetchAPIData, 1000);
}else{
clearTimeout(apiTimeout);
// Failure case. If required, alert the user.
}
})
.fail(function(){
clearTimeout(apiTimeout);
// Failure case. If required, alert the user.
});
}
#AmitJS94, there's a detailed section on how to stop an interval that adds onto the methods that GavKilbride mentioned in this article.
The author says to add a state for a delay variable, and to pass in "null" for that delay when you want to pause the interval:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
Definitely read the article to get a better understanding of the details -- it's super thorough and well-written!
As Vasanth mention, I preferred to:
use setTimeout to measure the time between the end of the last request and the beginning of the next one
make the first request straight away, not after the delay
inspired by the answer from #KyleMit https://stackoverflow.com/a/64654157/343900
import { useEffect, useRef } from 'react';
export const useInterval = (
callback: Function,
fnCondition: Function,
delay: number,
) => {
const savedCallback = useRef<Function>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
let id: NodeJS.Timeout;
const tick = async () => {
try {
const response =
typeof savedCallback.current === 'function' &&
(await savedCallback.current());
if (fnCondition(response)) {
id = setTimeout(tick, delay);
} else {
clearTimeout(id);
}
} catch (e) {
console.error(e);
}
};
tick();
return () => id && clearTimeout(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay]);
};
WORKS: Using fnCondition inside which can be a condition based on the response from the last request.
//axios-hooks
const {
data,
isLoadingData,
getData,
} = api.useGetData();
const fnCondition = (result: any) => {
const randomContidion = Math.random();
//return true to continue
return randomContidion < 0.9;
};
useInterval(() => getData(), fnCondition, 1000);
DOES NOT WORK: Passing delay as null to stop useInterval like this does not work for me
with this code: https://www.aaron-powell.com/posts/2019-09-23-recursive-settimeout-with-react-hooks/
(You might get the impression it works, but after a few starts/stops it breaks)
const [isRunning, setIsRunning] = useState(true);
const handleOnclick = () => {
setIsRunning(!isRunning);
};
useInterval(() => getData(), isRunning ? 1000 : null);
<button onClick={handleOnclick}>{isRunning ? 'Stop' : 'Start'}</button>
Sum up: I'm able to stop useInterval by passing fnCondition, but not by passing delay=null
Here's a simple, full solution, that:
Polls every X seconds
Has the option of increasing the timeout each time the logic runs so you don't overload the server
Clears the timeouts when the end user exits the component
//mount data
componentDidMount() {
//run this function to get your data for the first time
this.getYourData();
//use the setTimeout to poll continuously, but each time increase the timer
this.timer = setTimeout(this.timeoutIncreaser, this.timeoutCounter);
}
//unmounting process
componentWillUnmount() {
this.timer = null; //clear variable
this.timeoutIncreaser = null; //clear function that resets timer
}
//increase by timeout by certain amount each time this is ran, and call fetchData() to reload screen
timeoutIncreaser = () => {
this.timeoutCounter += 1000 * 2; //increase timeout by 2 seconds every time
this.getYourData(); //this can be any function that you want ran every x seconds
setTimeout(this.timeoutIncreaser, this.timeoutCounter);
}
Here is a simple example using hooks in function component and this will refresh your data in a set interval.
import React from 'react';
import { useEffect, useState } from 'react';
export default function App() {
let [jokes, setJokes] = useState('Initial');
async function fetchJokes() {
let a = await fetch('https://api.chucknorris.io/jokes/random');
let b = await a.json();
setJokes(b.value);
}
// Below function works like compomentWillUnmount and hence it clears the timeout
useEffect(() => {
let id = setTimeout(fetchJokes, 2000);
return () => clearTimeout(id);
});
return <div>{jokes}</div>;
}
or, you can use axios as well to make the API calls.
function App() {
const [state, setState] = useState("Loading.....");
function fetchData() {
axios.get(`https://api.chucknorris.io/jokes/random`).then((response) => {
setState(response.data.value);
});
}
useEffect(() => {
console.log("Hi there!");
let timerId = setTimeout(fetchData, 2000);
return ()=> clearInterval(timerId);
});
return (
<>
This component
<h3>{state}</h3>
</>
);
}

Categories

Resources