I'm trying to create a React app that will show time and weather based on user input.
To get weather data I send API request containing a name of the city to openweathermap and it returns json containing coordinates(lat, long) which I then use to make another request to ipgeolocation API to get the timezone of this coordinates.
Clock.jsx
const Clock = (props) => {
const [clock, setClock] = useState()
useEffect(() => {
setInterval(() => {
let time = new Date().toLocaleTimeString('en-US', { timeZone: props.timezone });
setClock(time)
console.log(time)
console.log(clock)
}, 1000);
})
return (
<div className={props.className}>
{clock}
</div>
)
}
After cahnging the timezone by making new API request time starts glitching. It frequently changes values between old and new time. What could be the problem?
console.log(time)
console.log(clock)
I can see a couple of issues:
First, you're creating a new interval on every render of the component, because you're not populating the right dependencies in useEffect. Since you're depending on props.tinmezone to update the interval, it should be added to the dependency array. To fix this, you can add props.timezone to useEffect's dependency array.
Second, you're not clearing the interval in your cleanup part of the useEffect. To fix this, return clearInterval() in useEffect.
Here's a working code snippet fixing both issues
useEffect(() => {
const interval = setInterval(() => {
let time = new Date().toLocaleTimeString("en-US", {
timeZone: props.timezone
});
setClock(time);
}, 1000);
return () => clearInterval(interval);
}, [props.timezone]);
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 am working on react dashboard which is to be displayed on large TV. It works fine for a single day. But when time reaches midnight dashboard gets stuck on same day and won't roll over to next day.
My approach
import React from 'react'
import moment from "moment";
function UpdateDate() {
const [{ startDate, endDate }] = useStateValue();
useEffect(() => {
var daterollOver = setInterval(function () {
var now = moment().format("HH:mm:ss");
if (now === "00:00:00") {
window.location.reload();
}
}, 1000);
return () => {
clearInterval(daterollOver);
}
}, [])
var url=`http:domain/live-dashboard?from=${startDate}&to=${endDate}`;
return (
<div>
<iframe
className="embed-responsive-item"
src={url}
></iframe>
</div>
)
}
export default UpdateDate;
I am using react context api to manage the state. Below is the section of the reducers.js which takes current time from moment.
import moment from "moment";
export const initialState = {
startDate: moment().startOf("day").local();,
endDate: moment().endOf("day").local();,
}
What would be the best option to refresh the page the at midnight which would update startDate & endDate and roll over to next day when time is midnight?
I'm naive when it comes to React myself, but from my understanding, you can use the useState hook to get/store things there.
I'd get the difference between now and midnight (represented here as endOf("day").add(1, 'ms')), then just set a timeout for that long. Don't bother checking every second or every minute or every hour. You know how much time needs to elapse, let that elapse. The setTimeout function is not terribly accurate, but on these scales it doesn't really matter. I wouldn't even check; just refresh. In the highly unlikely event it's too early, it will recalculate the next timeout to be very quick and resolve itself.
Using a state variable as the src of the iframe will cause a rerender of the HTML when the url changes, but that's fine -- you were reloading the page previously, this is less destructive than that.
I'm not sure if this works; I didn't bother creating a sandbox. Try it out, see if it helps. If it doesn't, please do create a sandbox (jsFiddle, codepen, whatever) to show it not working.
import React from 'react'
import moment from "moment";
function UpdateDate() {
function generateUrl() {
return `http:domain/live-dashboard?from=${moment().startOf("day")}&to=${moment().endOf("day")}`;
}
const [url, setUrl] = useState(generateUrl());
useEffect(() => {
let lastTimeout = 0;
let setupReload = function() {
const timeUntilMidnight = moment().diff(moment().endOf("day").add(1, "ms"), "ms");
return setTimeout(function() {
setUrl(generateUrl());
lastTimeout = setupReload();
}, timeUntilMidnight);
};
lastTimeout = setupReload();
return () => {
clearInterval(lastTimeout);
}
}, []);
return (
<div>
<iframe className="embed-responsive-item"
src={ url }>
</iframe>
</div>
);
}
export default UpdateDate;
I don't see you using startDate or endDate from context in your setInterval, so don't see the relevance of that? It's hard to say what's not working for sure but I would suggest a slightly different approach. Rather than checking every second if the time is exactly midnight... instead, I would check if the time is PAST midnight (using >=), and then also store the last time the page was refreshed, maybe in localstorage. Then you will change your logic to: is it after midnight, and more than 8 hours (or 23 hours... w/e) have elapsed since the last page refresh? Refresh the page. Something like that.
I'm working on a react project, and i've been trying to have Message components render at a certain pace, this is what i have now:
const [chatMessages, setChatMessages] = useState([])
function handleSubmit(event) {
event.preventDefault()
const input = inputValue;
setinputValue("")
if (input && inputHandler(input)) {
const newMessages = inputHandler(input)
const interval = setInterval(() => {
if (!newMessages.length) return clearInterval(interval)
setChatMessages([...chatMessages, newMessages.shift()])
}, 500);
}
}
inputHandler() returns an array of objects that i want to add individually to the state, setChatMessages() changes the state, and then the chatMessages state gets mapped to create my Message components.
This code works properly for single messages, but if inputHandler() returns more than 1 message, only one new message is added, and it cycles between all the messages in the array until it stops at the last one.
It seems that the chatMessages state isn't updating between iterations, anyone knows how i can work around this?
First, cache newMessages in a queue after const newMessages = inputHandler(input). Second, check the queue every 500ms, you can use an array to do that. If the queue is not empty, pop the first one.
Ok so i managed to behave like i want it to with this:
let interval = setInterval(() => {
if (!newMessages.length) return clearInterval(interval)
chatMessages = [...chatMessages, newMessages.shift()]
setChatMessages([...chatMessages])
}, 500);
feel like this code is a war crime, but it works
I have an issue which I'm beginning to suspect has no solution unless I drop React and return to jQuery. I want to create an app that is similar to https://tenno.tools/ or https://deathsnacks.com/wf/ These are sites which grab JSON data and update periodically.
I want to make a react app that uses axios to refresh the data once per minute with setTimeout, since the data changes often.
axiosFunc = () => {
axios.get('https://api.warframestat.us/pc').then(results => {
this.setState({
alerts: results.data.alerts
});
setTimeout(this.axiosFunc,1000 * 60);
})
}
componentDidMount() {
this.axiosFunc();
}
Next I need to use map to cycle through the alert array's objects and make individual components based off the objects' data that are active.
render() {
return (
<main className="content">
<header>{this.state.whichEvent.toUpperCase()}</header>
{this.state.alerts.map(alert => {
//Variables that pull time based data from the objects go here, and go into the component as props
<AlertsBox key={alert.id}/>
})}
</main>
);
}
Then I use the props and state within the component to make a timer, since the data from the JSON file have expiration dates...
let timer = () => {
//Extract the data from the original string
//Convert the UTC to locale time
let seconds = Math.round((this.state.eta/1000) % 60);
let minutes = Math.floor( (this.state.eta/1000/60) % 60 );
let hours = Math.floor( (this.state.eta/(1000*60*60)) % 24 );
let days = Math.floor( this.state.eta/(1000*60*60*24) );
return `${days >= 1? days + " days" : ""} ${hours >= 1? hours + " hrs" : ""} ${minutes} min ${seconds} sec`
}
And all of this works. I'm able to see the dynamic data from the JSON as they come in and leave, as well as the corresponding time. Now I just need to use setInterval in order to get the timer to tick every second. Is this possible? I asked a similar question here
How can I return values once per second using react and axios?
But again, I'm beginning to suspect that this isn't actually possible. Is it?
You'll want to use setInterval on the axiosFunc, no need to set that up inside the network request. Here's an example that calls your API every 5 seconds and renders a formatted date.
class Example extends React.Component {
constructor() {
super();
this.state = { alerts: [] };
}
axiosFunc = () => {
axios.get('https://api.warframestat.us/pc').then(results => {
this.setState({
alerts: results.data.alerts,
});
console.log('Updated the data!', results);
});
};
timer = time => {
// Your timer code goes here, just printing out example data here.
const date = new Date(time);
return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
};
componentDidMount() {
this.axiosFunc();
this.interval = setInterval(this.axiosFunc, 5000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
if (!this.state.alerts.length) {
return <div />;
}
// Sorting the alerts every time we render.
const latest = this.state.alerts.sort((a, b) => {
return new Date(b.activation) - new Date(a.activation);
})[0];
return <div>{this.timer(latest.activation)}</div>;
}
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.17.1/axios.min.js"></script>
<div id="root"></div>
It's definitely possible. As you said, all of this works - which part is actually giving you trouble? Are you getting an error anywhere?
Personally, I'd think about using Redux in addition to React in an app like this because I like to separate the fetching of data from the presentation of data, but that's all just personal preference. I have an example app that uses setInterval directly in a React component, in case the move from setTimeout to setInterval is causing you pain.