Every programmers worst nightmare is (or should be) the scenario where a date based bug is caught years later. Why? Because that bug has now probably ended up persisting to your production database and now you don't just have to fix the code but you have to fix the data too, which is an extremely costly recovery that might require apologizing to your customers.
Which is why I was a pretty disappointed to see this bug still open in the react-datepicker
issues list.
You might ask: why don't they just fix the bug?
Well... it's a logical bug, not a coding error. In fact, this "bug" is probably in every date picker library on the internet... unless it uses the techniques I will be showing below.
Thankfully, you can prevent logical errors by using domain driven design and types that help enforce the logic (stay with me, don't get scared off by types/OO/DDD concepts).
So follow along and I'll show a way to continue to use the robust and highly-accessible react-datepicker
library without introducing logical bugs that are hard to track down.
How would that help?
"say what you mean, mean what you say"
Clear communication is vitally important. Let's imagine that I want to buy my friend a birthday card. So I ask them what their birthday is. Should I expect them to respond with "My birthday is January 1st 1990 at 2:13am" or would you expect them to say "January 1st"?
Of course you wouldn't expect to get the hours or minutes back because you didn't ask that person for the moment that doctor announced the birth of the baby.
I like to imagine that the same clarity of communication can and should be applied to programming.
So a birthday is a LocalDate
and the moment they were born is a ZonedDateTime
. However, if I wanted to know the moment they were born without time zone information, it would be an Instant
(think ISO format in the GMT time zone).
By specifying what you want in the code, you make it clear what is accepted. Would you want someone to pass a number
when you really wanted a string
? Being clear on your inputs allows you to catch errors at compile time instead of finding the bug in production or having to write expensive unit tests (that might be brittle and therefore leave you with the bug in production anyway). Follow along below to see how you can prevent bugs by simply saying "no, I do not accept a Date, I only accept a LocalDate."
Hold up, I have to learn new terms like LocalDate?
Sure, if you want to prevent extremely expensive bugs in your code you do. But don't worry, once you learn them it's hard to not think in the terms of Instant
, LocalDate
, and ZonedDateTime
. And I have a cheat sheet for you on my prior article if you want to catch up the terms.
And frankly, you should take 2 minutes to learn the terms since the future is coming when it will be a code smell to use Date or even a library that exposes a Date. That's because the JavaScript community is finalizing the Temporal RFC spec. A polyfill is being worked on already, but soon it will be in all of our browsers so you won't need to use the native JS Date class. And Java has had these concepts since since Java 8.
While we wait for the Temporal RFC spec to be adopted and implemented in browsers, we can use JsJoda which implements the Java 8 / Threeten spec in JavaScript/TypeScript since JsJoda uses all of the same concepts.
Okay, show me
So first, a simple example of how this works. Using our "What is your birthday" example, we can mock up this code. Note: I'm using TypeScript because it enforces the concepts at compile time, but the JsJoda library itself enforces the concepts at runtime so that we get the best of both.
This is essentially the conversation above but in code:
import {LocalDate} from "@js-joda/core"
// Notice that the type of the parameter forces us to box the type first
const saveDateToDatabase = async (day: LocalDate) => {
await db.save(day)
}
const whatIsYourBirthday = async (inputFromKeyboard: string) => {
try{
// Okay, it's time to try to see if the string can become a LocalDate
const day = LocalDate.parse(inputFromKeyboard);
} catch(err){
throw new Error(`It seems like what you entered is not a date.
Maybe it has too much (like it shouldn't have the year or time).
See the full error: ${err}`);
}
await saveDateToDatabase(day);
}
If you want to play around with JsJoda, I'd recommend opening up the homepage since it has the library loaded into the window object for you to experiment. Note: the doc pages do not.
I think you'll find that the learning curve is a bit steep; however, the cost savings long term are significant. Think of it as a "slow down so you can speed up" type of situation.
Okay, I'm sold... but can we make it easier?
If you like the idea of clarifying when you mean a day vs an moment in a time zone then you still might want to make it easier on yourself to start with those terms. You might not want to have to do that necessary conversion between the JS standard Date
object when you're in a callback of a form element. I can't blame you.
So what if you never had to use Date
at all?
The following is a CodePen that demonstrates a way to encapsulate the conversion so that you're always dealing with the safer concepts.
Essentially you wouldn't every directly render react-datepicker
, but instead would render a custom LocalDatePicker
.
Notice that the above CodePen uses LocalDate
because the date picker does not allow the user to select the time.
So let's show another example but this time a ZonedDateTimePicker
where we're using react-datepicker
's showTimeInput prop. An example where this would be helpful would be an insurance adjuster calling you to ask "what time did your car get into a collision?" You would want to know the ZonedDateTime
that this occurred, so that's the JsJoda type we'll use in the CodePen.
It's important to call out that I'm not showing an example of an InstantPicker
since the question being asked is "what time did your car get hit in your timezone?" So that's why it would be a ZonedDateTime
. Again, don't get scared by the differences between the concepts-- once you speak the language you'll find it hard to use ambiguous terms like "date."
I'd encourage you to look at the source code of that CodePen above (under the "Babel" tab); however, to understand how this encapsulation solves the logical bug, consider this portion:
if (!!selected && !ZoneId.from(selected).equals(zone)) {
throw new Error(
`The provided date ("${selected}") was not in the expected ZoneId ("${zone}")`
);
}
What that does is ensure that if a value for the selected date comes back in a different time zone / offset, the component will stop in it's tracks. It's not the ideal behavior for your users, but it's a good example of how it can prevent a bug from going un-noticed. This is only possible due to a beautifully expressed domain language that expresses the concept of a Zone. You can't do that with regular ole' Date
!
Summary
If you chose to adopt this approach, you and your peers will be able to have a clear conversation about if the current feature you're writing needs ZonedDateTime
or LocalDatePicker
. By doing that you'll protect your software and your users for years to come.
Attribution
Note: I would probably build this into a library but I'm maxed out on other maintainer work at the moment. So if you choose to turn my codepen into a library, please just share a link to this article! :)
By the way, you know that hypothetical scenario I mentioned at the top? That actually happened to me. I had to spend nights and weekends for over a month to fix the incorrect data in the database caused by a previous, long-gone developer. Thankfully, a mentor shared with me the concepts I described in this article so I was able to improve the code more easily. So be like that mentor and share this article with friends and coworkers so they can prevent bugs too! :)
Top comments (0)