I have given a setInterval function inside a useEffect as it has to dispatch an action for every 28 min, and this setInterval will start only when the user is logged in (isLogged variable provides us whether the user is logged in or not), it is working as expected, but if I open a new tab, it starts a new 28 min interval in that new tab which I don't expect.
My Requirement -
Every 28 min there should be a dispatch event.
If the user logs in to one tab and works for 20 min and opens a new
tab, in the next 8 min dispatch event should occur in both the tabs
at the same time.
Code
const isLogged = useSelector((state) => state.userReducer.loggedIn); // provide boolean value
const intervalId = useRef(null);
useEffect(() => {
intervalId.current = setInterval(() => {
// To check in between if user is logged out and to stop setInterval
if(isLogged === false){
clearInterval(intervalId.current);
return;
}
dispatch(refreshUserToken());
}, 1000 * 60 * 28);
return () => {
clearInterval(intervalId.current)
}
},[isLogged])
I don't have any idea on how to do this concept, could someone please help me to solve this problem?
Thanks in Advance !!
Related
I have a chat app on React and when chat can not connect, reconnect modal (ant d) is opened
And I want that, when I click the ''reconnect``` button, the countdown must work. But it only works when I click, and after it stops.
I think React can not render because on the console it repeats.
Maybe it depends on Websocket.
My codes
const [countDown, setCountDown] = useState(60);
const [reconnectModal, setReconnectModal] = useState(false);
const reconnectFunction = () => {
connect(); // connect is for Websocket. I can connect the chat with this function.
setInterval(() => {
setCountDown(countDown - 1);
}, 1000);
};
<Modal
visible={reconnectModal}
okText={`Reconnect ${`(${countDown})`}`}
cancelText="Close"
onOk={reconnectFunction}
onCancel={() => setReconnectModal(false)}
>
Connection failed. Please try again
</Modal>
It is because when you set the interval it will convert the countDown with the actual value (default here is 60).
So when it update the value of countDown, it will not update this value in the interval.
I think you can simply change to :
setInterval(() => {
setCountDown((v) => v - 1);
}, 1000);
As the v is always the last value of the state.
Working example here.
Don't forget to handle when the count is at 0. And maybe have the interval in a ref to cancel it when you are connected and therefore no need to continue the interval.
So right now I have a verifyScreen which users are sent to when they don't have a verified email. I have a setInterval function that runs every 5 seconds to see if the user has confirmed their email.
I also need to have a second setInterval for the resend email button in case a user has not received it. It counts down by 1 until it hits 0 and the user can resend an email.
If I have one or the other in my useEffect everything works fine. They also won't run together if the other is running so I need some help understanding how setInterval works behind the scenes and how I can fix my issue. Thank you.
const [emailTimer, setEmailTimer] = useState(0);
const [, setUser] = useAtom(userAtom);
useEffect(() => {
const userTimer = setInterval(async () => {
const refreshedUser = auth().currentUser;
console.log('checked user');
if (refreshedUser?.emailVerified) {
clearInterval(userTimer);
setUser(refreshedUser);
} else {
await auth().currentUser?.reload();
}
}, 5000);
const resendEmailTimer = setInterval(() => {
if (emailTimer <= 0) {
clearInterval(resendEmailTimer);
} else {
console.log('second subtracted');
setEmailTimer(emailTimer - 1);
}
}, 1000);
return () => {
clearInterval(userTimer);
clearInterval(resendEmailTimer);
};
}, [emailTimer]);
If you know the solution and can also explain to me the why behind all this, I would really appreciate that.
Edit, my component which sets the email timer:
<View style={{ paddingBottom: 10 }}>
<PrimaryButton
disabled={emailTimer > 0}
onPress={async () => {
setEmailTimer(60);
await handleSendEmailConfirmation();
}}>
<CustomText
text={emailTimer > 0 ? `${emailTimer}` : 'Resend email'}
color={theme.colors.white}
/>
</PrimaryButton>
</View>
<OutlinedButton
onPress={async () => {
await handleLogOut();
}}>
<CustomText text="Cancel" />
</OutlinedButton>
</View>
Edit:
What happens with the code right now:
The component renders, and the user interval runs every ~5 seconds.
I click on the resend email button, which triggers the email timer every 1 second.
While the email counter is going, the user counter pauses until it hits 0.
Then the user counter resumes.
Console logs for more description:
Edit 2: Ok I managed to get both intervals working at the same time by moving the user interval outside of the useEffect. This way the email interval triggers when the state updates and it's added to the dependency array.
The one problem is the user interval that is outside of the useEffect sometimes triggers twice in the console. Not sure if that's because I'm running on a simulator or an error.
const [emailTimer, setEmailTimer] = useState(0);
const [, setUser] = useAtom(userAtom);
const userTimer = setInterval(async () => {
const refreshedUser = auth().currentUser;
console.log('checked user');
if (refreshedUser?.emailVerified) {
clearInterval(userTimer);
setUser(refreshedUser);
} else {
await auth().currentUser?.reload();
}
}, 10000);
useEffect(() => {
const resendEmailTimer = setInterval(() => {
if (emailTimer <= 0) {
clearInterval(resendEmailTimer);
} else {
console.log('second subtracted');
setEmailTimer(emailTimer - 1);
}
}, 1000);
return () => {
clearInterval(userTimer);
clearInterval(resendEmailTimer);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [emailTimer]);
If you have two setIntervals, they work totally independently. Of course, if both intervals change the same variable, both will try to update the variable at the same time and sometimes, it could cause an unexpected result, but that's not your case.
One huge issue your code has is, you're creating an interval every time setUser or emailTimer gets updated. setUser will not change after the initialization but emailTimer gets updated frequently and thus will cause hundreds of setIntervals to be created. If it works correctly, that's a miracle. I'm not sure how it could work when you have one interval.
Correct approach is adding them one time when the page mounts:
useEffect(() => {
const userTimer = setInterval(...);
const resendEmailTimer = setInterval(...);
}, []); // this empty [] is the key
ESLint may complain about react-hooks/exhaustive-deps rule but it doesn't make sense in your case, so simply suppress it. There's a lot of debate on whether this rule should be strictly enforced or not, but that's another topic.
Side note: I don't like the naming of your emailTimer variable. All other intervals have xxxTimer format, and it could easily mislead emailTimer is also an interval, so maybe consider renaming your state variable to emailTimerCount.
I have an app where one user hosts a game, and then other users can vote on questions from the host. From the moment the host posts the question, the players have 20 seconds to vote.
How can I show a countdown timer on all player's screens and keep them synchronized with the host?
Many developers get stuck on this problem because they try to synchronize the countdown itself across all users. This is hard to keep in sync, and error prone. There is a much simpler approach however, and I've used this in many projects.
All each client needs to show its countdown timer are three fairly static pieces of information:
The time that the question was posted, which is when the timer starts.
The amount of time they need to count from that moment.
The relative offset of the client to the central timer.
We're going to use the server time of the database for the first value, the second value is just coming from the host's code, and the relative offset is a value that Firebase provides for us.
The code samples below are written in JavaScript for the web, but the same approach (and quite similar code) and be applied in iOS, Android and most other Firebase SDKs that implement realtime listeners.
Let's first write the starting time, and interval to the database. Ignoring security rules and validation, this can be as simple as:
const database = firebase.database();
const ref = database.ref("countdown");
ref.set({
startAt: ServerValue.TIMESTAMP,
seconds: 20
});
When we execute the above code, it writes the current time to the database, and that this is a 20 second countdown. Since we're writing the time with ServerValue.TIMESTAMP, the database will write the time on the server, so there's no chance if it being affected by the local time (or offset) of the host.
Now let's see how the other user's read this data. As usual with Firebase, we'll use an on() listener, which means our code is actively listening for when the data gets written:
ref.on("value", (snapshot) => {
...
});
When this ref.on(... code executes, it immediately reads the current value from the database and runs the callback. But it then also keeps listening for changes to the database, and runs the code again when another write happens.
So let's assume we're getting called with a new data snapshot for a countdown that has just started. How can we show an accurate countdown timer on all screens?
We'll first get the values from the database with:
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
...
});
We also need to estimate how much time there is between our local client, and the time on the server. The Firebase SDK estimates this time when it first connects to the server, and we can read it from .info/serverTimeOffset in the client:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
});
In a well running system, the serverTimeOffset is a positive value indicating our latency to the server (in milliseconds). But it may also be a negative value, if our local clock has an offset. Either way, we can use this value to show a more accurate countdown timer.
Next up we'll start an interval timer, which gets calls every 100ms or so:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
const interval = setInterval(() => {
...
}, 100)
});
Then every timer our interval expires, we're going to calculate the time that is left:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
const interval = setInterval(() => {
const timeLeft = (seconds * 1000) - (Date.now() - startAt - serverTimeOffset);
...
}, 100)
});
And then finally we log the remaining time, in a reasonable format and stop the timer if it has expired:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
const interval = setInterval(() => {
const timeLeft = (seconds * 1000) - (Date.now() - startAt - serverTimeOffset);
if (timeLeft < 0) {
clearInterval(interval);
console.log("0.0 left)";
}
else {
console.log(`${Math.floor(timeLeft/1000)}.${timeLeft % 1000}`);
}
}, 100)
});
There's definitely some cleanup left to do in the above code, for example when a new countdown starts while one is still in progress, but the overall approach works well and scales easily to thousands of users.
Ok, I have a useInterval (custom hook) that will delete a document from firestore after a given time is milliseconds. It works fine if I set it to 10 seconds, 1 minute.
However, when I set it to delete the document a month after it was created, it looks like it creates the document and gets deleted right away.
How can I set my interval to delete the document after a month from when it was created?
const docRef = firestore.doc(`posts/${id}`)
const deleDoc = () => docRef.delete();
//******************************************************************* */
//? Deleting post after a month(time managed in milliseconds)
const now = createdAt.seconds * 1000;
const monthFromNow = 2628000000;
const then = now + monthFromNow;
const timeLeft = then - Date.now();
//?custom hook
useInterval(() => {
docRef.delete();
}, timeLeft);
You can create a task which hits one of your cloud function endpoint using Cloud Task. You can set it to hit the API every 1 min.
In the API you can pull up all the documents which are older than 1 month and delete them using the above logic.
My app is a game where a user has 30 mins to finish....node backend
Each time a user starts a game then a setInterval function is triggered server side....once 30mins is counted down then I clearInterval.
How do I make sure that each setInterval is unique to the particular user and the setInterval variable is not overwritten each time a new user starts a game? (or all setInterval's are cleared each time I clear).
Seems like I might need to create a unique "interval" variable for each new user that starts game??
Below code is triggered each time a new user starts a game
let secondsLeft = 300000;
let interval = setInterval(() => {
secondsLeft -= 1000;
if (secondsLeft === 0) {
console.log("now exit");
clearInterval(interval);
}
}, 10000);
Thanks!!
We used agenda for a pretty big strategy game backend which offers the benefit of persistence if the node app crashes etc.
We incorporated the user id into the job name and would then schedule the job, along with data to process, to run at a determined time specifying a handler to execute.
The handler would then run the job and perform the relevant tasks.
// create a unique jobname
const jobName = `${player.id}|${constants.events.game.createBuilding}`;
// define a job to run with handler
services.scheduler.define(jobName, checkCreateBuildingComplete);
// schedule it to run and pass the data
services.scheduler.schedule(at.toISOString(), jobName, {
id: id,
instance: instance,
started: when
});
Worked pretty well and offered decent protection against crashes. Maybe worth considering.
First: Concurrent Intervals and Timers are not the best design approach in JS, it is better to use one global timer and a list of objects storing the start, end, userid etc and update these in a loop.
Anyway. To have your interval id bound to a certain scope, you can use a Promise like so:
const createTimer = (duration, userid) => new Promise(res => {
const start = new Date().getTime();
let iid;
(function loop () {
const
now = new Date().getTime(),
delta = now - start
;
//elapsed
if (delta >= duration) {
clearTimeout(iid);
res(userid);
//try again later
} else {
iid = setTimeout(loop, 100)
}
})();
});
This way each timer will run »on its own«. I used setTimeout here since that wont requeue loop before it did everything it had to. It should work with setInterval as well and look like that:
const runTimer = (duration, userid, ontick) => new Promise(res => {
const
start = new Date().getTime(),
iid = setInterval(
() => {
const delta = new Date().getTime() - start;
if (delta < duration) {
//if you want to trigger something each time
ontick(delta, userid);
} else {
clearInterval(iid);
res(userid);
}
}, 500)
;
});
You do not even need a promise, a simple function will do as well, but then you have to build some solution for triggering stuff when the timer is elapsed.
Thanks #Chev and #philipp these are both good answers.
I was also made aware of a technique where you use an array for the setInterval variable.....this would make my code as follows;
let intervals = []
let secondsLeft = 300000;
intervals['i'+userId] = setInterval(() => {
secondsLeft -= 1000;
if (secondsLeft === 0) {
console.log("now exit");
clearInterval(interval);
}
}, 10000);
Does anyone else foresee this working?.
UPDATE 6.56pm PST.....it works!!