Let's Talk About Time
Time is super tricky to account for in software, and one of the most common issues in frontend applications is developers forget that time keeps passing when the page is open.
Take a look at Falsehoods programmers believe about time for a starting point on the assumptions developers make.
It's really common to write a component that looks like this:
const formatter = new Intl.DateTimeFormat("en-us", {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
});
const MyDateComponent = () => {
const date = new Date();
return formatter.format(date);
};
See MDN Intl.DateTimeFormat for more on the date formatter used.
The issue with this component is that it doesn't update when the seconds change. This isn't as much an issue if we're not displaying seconds, but even hours and days can pass while browser tabs are open.
useDateTime
To solve this problem, I wrote useDateTime
, a React hook that tracks the time to a specified precision (second/minute/hour/day), triggering a state change on each tick
.
Using useDateTime
to fix MyDateComponent
, we get the following:
const MyDateComponent = () => {
const date = useDateTime("second"); // second | minute | hour | day
return formatter.format(date);
};
This component now updates every second, keeping it accurate. We probably only want updates for every second in a clock component, and should avoid this frequency of updates for expensive renders. Outside of clocks, updating by the hour/day is much more common and something we should plan for as frontend engineers.
You can take a look at the implementation of useDateTime
in this codesandbox:
The implementation uses date-fns
but could be rewritten with any date library (e.g. Moment/Luxon/Day.js)
A similar hook could be used to track relative times by modifying
msUntilNext
to determine the nexttick
in increasing intervals.
Disclaimer
This component attempts to update immediately after the next tick
at the specified precision. Javascript's setTimeout
API does not guarantee that the timeout will trigger precisely on the target time, so the precision of this hook is also limited. Here's a good Stack Overflow Q&A summarizing this problem and a workaround.
Top comments (5)
I just wanted to write such hook myself and can now save my time, thanks!
I think there is a little bug in the effects initial cleanup, when its still mounted but the threshold prop changes. I think the
clearTimeout
should be outisde thethreshold
check. What do you think?Also I wonder how easy the hook could be extended to support thresholds like half hours or full 15 minute intervals?
Hi, thanks for sharing, and I'm glad it's useful to you! Full disclosure, this is a cleanup of the first custom hook I ever wrote around 2 years ago. I glossed over that part in my cleanup because I assumed it was right, but I wrote it before I fully understood hooks (even now, with scenarios like this, they're kinda tricky).
Looking closer, it seems like the
clearTimeout
is completely redundant with the cleanup function - which is always called whenever the threshold changes, or when the component unmounts. I've removed it which should help with the confusion. Thanks for flagging, I wouldn't have found this redundancy otherwise.For the custom intervals, I don't think it would be hard to implement, you'd just need to change some of the math with
startOfThreshold
andmsUntilNext
. Making it fully featured in that sense, you could even add an offset to account for something like "the 3rd minute of every hour".I think it would also be some extra work to anchor the 15 minutes against the hour, rather than any 15 minutes - though it could also be another hard coded threshold.
At some point I might write about testing this - since timeouts and intervals can be tricky to test, and there's a few interesting edge cases.
I have extended your hook, to support amounts of thresholds (before it was just 1), converted the hook to TypeScript and remove the extra cleanup as you suggested:
Then I wanted to add some tests, but I can't get it to run properly, let alone pass green.
Any idea how to test our hook?
OK, I have the tests working now and they instantly revealed some bugs in the hook. Getting tests to run properly (for a react-native project if that matters) was the main challenge.
It involved a custom jest-preset to work around some promise related bug:
See solution and discussion
jest-preset.js
This test now runs green:
A bug it revealed was that the hook reported the estimated future date before the timeout actually happened. In your component this was not a problem, you did not know you were one second ahead of system time ;) That happened because it called
setDate
outside the timeout callback.So now everything works! Thanks again for the initial work!
I’ll take a look, but this change likely breaks its ability to immediately respond to threshold changes e.g. second to minute.
I also wouldn’t expect the setDate to change unless the time has actually passed by more than a second since it goes to the start of the threshold.
Again, I haven’t confirmed this yet, but that’s my intuition.EDIT: I have confirmed the tests pass without your changes. The break may be caused by your added functionality
You can pull the tests down from useDateTime Code Sandbox with Tests
in a new Create React Appwith "export to zip" to confirm (Code Sandbox doesn't seem to play well with@jest/globals
, andexport to Zip wasn't working for me either).I've included a test that I believe will fail with your change, but you can confirm that for your implementation.