Managing Dates and Times in JavaScript Using date-fns

James Hibbard
Share

Working with dates in JavaScript is a pain. Native date methods are often verbose and occasionally inconsistent. But good news is at hand. The date-fns library provides a simple yet comprehensive toolset for managing dates and times in JavaScript. 

date-fns is a lightweight alternative to moment.js or native JavaScript date manipulation. It offers a great set of methods for common tasks such as:

  • Formatting dates
  • Internationalization (i18n)
  • Comparing dates
  • Sorting dates
  • Finding the gap between two dates
  • Converting between timezones

Let me give you an example of how useful it can be: This is the accepted answer to a Stack Overflow question asking how to get last day of the month:

var t = new Date();
alert( new Date(t.getFullYear(), t.getMonth() + 1, 0, 23, 59, 59) );

Of course that works, but it’s not immediately obvious what the numbers after getMonth represent. Now contrast that with the considerably more readable:

const today = new Date();
console.log( lastDayOfMonth(today) );

That lastDayOfMonth method is one provided by date-fns, a self-proclaimed comprehensive toolset for manipulating JavaScript dates in the browser and Node.js.

In this article I’m going to show you how to get up and running with date-fns. After reading you’ll be able to drop it into your projects and take advantage of its many helper methods to manipulate dates with ease. This will make code like t.getMonth() + 1, 0, 23, 59, 59 a thing of the past.

Why Not Moment.js?

Moment.js is a fantastic library for working with dates in JavaScript — it has many great features and offers a whole host of useful utilities. It is, however, is not without its critics.

Many people cite the fact that Moment objects are mutable (i.e. operations like add, or subtract change the original Moment object) as being confusing for developers and a source of bugs.

It has also come under fire for its large size. Moment doesn’t play well with modern “tree shaking” algorithms and if you require internationalization or time zone support, you can quickly find yourself with a rather large JavaScript bundle.

This has gone so far that Chrome’s dev tools now highlights the fact that using Moment can lead to poor performance. All of which has lead the Moment maintainers to place the project into maintenance mode and to discourage Moment from being used in new projects going forward.

This makes date-fns one of the best alternatives to Moment.js out there.

Installing date-fns

Since version two of the library, the only way to install date-fns is as an npm package.

npm install date-fns

Or via Yarn:

yarn add date-fns

You can use date-fns with both the CommonJS module system and also with ES modules:

// CommonJS
const { lastDayOfMonth } = require('date-fns');

or:

// ES Modules
import { lastDayOfMonth } from 'date-fns';

There is unfortunately currently no CDN version of date-fns available. Its removal and possible reinstatement is being discussed in this GitHub issue. But, that’s not to say you cannot use it in a browser, just that you’ll need to introduce a bundling step into your workflow.

Let’s look at how to do that now.

How Bundle date-fns for Use in a Browser

I’ll assume you have Node and npm installed on your machine. If not, please consult our tutorial on installing Node.

Next, install Parcel. This is a bundler (similar to Webpack), which will allow you to bundle up your JavaScript and serve it in a browser.

npm install -g parcel-bundler

Next, make a new project with a package.json file.

mkdir datefns
cd datefns
npm init -y

Install the date-fns library, as above:

npm install date-fns

Note: this will create a date-fns folder inside a node_modules folder in your project directory. If you look inside the date-fns folder, you will see lots more folders and files. Don’t worry though, we won’t be shipping much of this to the client. We’ll only be selecting the functions we need and then running everything through parcel to make a minified and tree shaken bundle.

Now create two files, index.html and index.js.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>date-fns</title>
  </head>
  <body>
    <script src="index.js"></script>
  </body>
</html>
import { lastDayOfMonth } from 'date-fns';

const today = new Date();
console.log(lastDayOfMonth(today));

Start parcel’s inbuilt development server:

parcel index.html

And navigate to http://localhost:1234. You won’t see anything displayed on the page, but if you open the browser’s console. you should that it has logged the last day of the current month.

When it comes to deployment, you can run:

parcel build index.js --experimental-scope-hoisting

to have Parcel output a minified and tree-shaken bundle in the dist folder.

Date-fns Basic Usage

Now that we’re up and running, let’s look at what date-fns can do.

One of the most common tasks when working with dates is the ability to format them nicely. We can do this with the date-fns format function.

Alter the HTML from our example page above to look like this:

<body>
  <h1>The date today is <span></span></h1>
  <script src="index.js"></script>
</body>

In index.js we want to import the format function, which we can then pass today’s date and a format string. We then want to output the result to the page.

import { format } from 'date-fns';

const today = new Date();
const formattedDate = format(today, 'dd.MM.yyyy');

document.querySelector('h1 > span').textContent = formattedDate;

Of course, we are not limited to a dd.MM.yyyy format, let’s try something different:

const formattedDate = format(today, 'PPPP');

This will format the output like so: Wednesday, September 16th, 2020. You can find a full list of formatting options in the docs.

Change Locale

If you have a website in multiple languages, then date-fns makes it simple to internationalize times and dates. Let’s greet our German guests:

<h1>Heute ist <span></span></h1>

And in the JavaScript file, we can import the German locale and pass it to the format function:

import { format } from 'date-fns';
import { de } from 'date-fns/locale';

const today = new Date();
const formattedDate = format(today, 'PPPP', { locale: de });

document.querySelector('h1 > span').textContent = formattedDate;

This will output something along the lines of: Heute ist Mittwoch, 16. September 2020.

It might seem complicated to require and pass locales as options, but unlike Moment.js which bloats your build with all the locales by default, date-fns forces developers to manually require locales as and when they are needed.

You can view a list of available locales by looking in the node_modules/date-fns/locale folder in your project.

Immutability, Pureness and Simplicity

One of the selling points for date-fns is that its functions are pure, and simple, to explain. This leads to easy to understand code, which is easier to debug when things go wrong.

Let me demonstrate this using Moment.js as a counter example. As mentioned before, dates in Moment are mutable, which can lead to unexpected behavior.

const moment = require('moment');
const now = new Date();
const mNow = moment(now);

mNow.add('day', 3);
console.log(mNow.toDate());
mNow.add(3, 'day');
console.log(mNow.toDate());

// 2020-09-19T10:08:36.999Z
// 2020-09-22T10:08:36.999Z

There are a couple of things to take note of here. Moment’s add function is not fussy about the order in which it accepts its arguments (although the first method will now throw a deprecation warning). But more confusing is that if you call add multiple times in a row you, won’t get the same result because Moment objects are mutable:

mNow.add(3, 'day'); // add 3 days
mNow.add(3, 'day'); // adds 3 **more** days

Now compare that to date-fns which keeps arguments in one order and always returns the same result, returning a new Date object for each call.

import { addDays } from 'date-fns';

const today = new Date();
const threeDaysTime = addDays(3, today);
const sixDaysTime = addDays(threeDaysTime, 3);

console.log(today); // Wed Sep 16 2020 12:11:55 GMT+0200
console.log(threeDaysTime); // Sat Sep 19 2020 12:12:58 GMT+0200
console.log(sixDaysTime); // Invalid Date

Also notice how the method name is more expressive (addDays instead of just add), keeping things consistent and having one method to do one thing and one thing only.

Comparing Dates

If you look at the list of posts on SitePoint’s JavaScript channel, you can see that some are listed as being published on a certain date, whereas others are listed as being published X days ago. It might take a while if you tried to implement this in vanilla JavaScript, but with date-fns this is a breeze – just use the formatDistance method.

Let’s compare two different dates.

import { formatDistance } from 'date-fns';

const startDate = new Date(2020, 8, 16); // (Sep 16 2020)
const endDate = new Date(2020, 11, 25); // (Dec 25 2020)
const distanceInWords = formatDistance(startDate, endDate);

console.log(`It is ${distanceInWords} until Christmas`);
// It is 3 months until Christmas

Notice how, when working with JavaScript, months are zero based (e.g. month 11 = December), but days count up from one. This trips me up time and again.

Working With Collections of Dates

Date-fns has some very handy helper methods which you can use to manipulate collections of dates in all kinds of ways.

Ordering a Collection of Dates

The following example uses compareAsc to sort dates into ascending order. To do this, it returns 1 if the first date is after the second, -1 if the first date is before the second or 0 if dates are equal.

import { compareAsc } from 'date-fns';

const date1 = new Date('2005-01-01');
const date2 = new Date('2010-01-01');
const date3 = new Date('2015-01-01');
const arr = [date3, date1, date2];
const sortedDates = arr.sort(compareAsc);

// [ 2005-01-01, 2010-01-01, 2015-01-01 ]

As you can see the dates are now in ascending order.

The counterpart method to compareAsc is compareDesc.

import { compareDesc } from 'date-fns';
...
const sortedDates = arr.sort(compareDesc);
// [ 2015-01-01, 2010-01-01, 2005-01-01 ]

Generating the Days Between Two Dates

To generate the days between two dates, you can use the addDays method we met previously, as well as the eachDayOfInterval helper which returns an array of dates within the specified range.

import { addDays, eachDayOfInterval } from 'date-fns';

const today = new Date();
const aWeekFromNow = addDays(today, 7);
const thisWeek = eachDayOfInterval(
  { start: today, end: aWeekFromNow },
);

console.log(thisWeek);

/*
[
  Wed Sep 16 2020 00:00:00 GMT+0200 (Central European Summer Time),
  Thu Sep 17 2020 00:00:00 GMT+0200 (Central European Summer Time),
  Fri Sep 18 2020 00:00:00 GMT+0200 (Central European Summer Time),
  Sat Sep 19 2020 00:00:00 GMT+0200 (Central European Summer Time),
  Sun Sep 20 2020 00:00:00 GMT+0200 (Central European Summer Time),
  Mon Sep 21 2020 00:00:00 GMT+0200 (Central European Summer Time),
  Tue Sep 22 2020 00:00:00 GMT+0200 (Central European Summer Time),
  Wed Sep 23 2020 00:00:00 GMT+0200 (Central European Summer Time)
]
*/

Finding the Closest Date

Finding the closest date to a certain date in an array of dates can be done using the closestTo method. This code snippet follows on from the previous example:

import { addDays, eachDayOfInterval, closestTo } from 'date-fns';
...
const christmas = new Date(2020, 11, 25);
const closestToChristmasDate = closestTo(christmas, thisWeek);

console.log(closestToChristmasDate);
// Wed Sep 23 2020 00:00:00 GMT+0200 (Central European Summer Time)

There’s also the closestIndexTo method if you want to get the index of the array instead.

Validating a Date

The final helper I want to look at is the isValid method which, as the name suggests, checks if a given date is valid.

However, because of the way JavaScript deals with dates, there are a couple of gotchas to be aware of:

import { isValid } from 'date-fns';

const invalidDate = new Date('2020, 02, 30');
console.log(isValid(invalidDate));
// true, lol, wut?

You would be forgiven for thinking that the above snippet should output false, as the 30th February, 2020 is obviously an invalid date. To understand what is happening, enter new Date('2020, 02, 30') in your browser’s console. You will see Sun Mar 01 2020 come back to you — JavaScript has taken the extra day from the end of February, and turned it into the 1st March (which is of course a valid date).

To work around this, we can parse the date before checking its validity:

import { isValid, parse } from 'date-fns';

const validDate = parse('29.02.2020', 'dd.MM.yyyy', new Date());
const invalidDate = parse('30.02.2020', 'dd.MM.yyyy', new Date());

console.log(validDate);
// Sat Feb 29 2020 00:00:00 GMT+0100 (Central European Standard Time)

console.log(invalidDate);
// Invalid Date

console.log(isValid(validDate));
// true

console.log(isValid(invalidDate));
// false

This can easily be extracted out into a little helper method, useful, for example, for validating user input in forms.

Time Zones

One disadvantage of date-fns is that it doesn’t currently have any time zone helper functions like Moment.js does, rather it returns the local time zone that the code is running on.

This Stack Overflow answer gives some background on how native Date objects don’t actually store “real time zone” data. In that thread you’ll notice that they mention a method of setting time zones natively in JavaScript. This isn’t a comprehensive solution, but it works for many scenarios that require only output conversion (from UTC or local time to a specific time zone).

new Date().toLocaleString("en-US", {timeZone: "America/New_York"});

Time zones are actually a complicated problem to solve which is why MomentJS has a separate library for it. There are plans afoot to add time zone support to date-fns, but at the time of writing, this is still a work in progress.

There is however a package available on npm (based on an unmerged pull request to date-fns) which adds time zone support for date-fns v2.0.0 using the Intl API. With 140k weekly downloads it seems popular, but at the time of writing, it hasn’t been updated for several months.

That said, here’s how you might use it:

npm i date-fns-tz
import { format, utcToZonedTime } from 'date-fns-tz';

const today = new Date(); // Wed Sep 16 2020 13:25:16
const timeZone = 'Australia/Brisbane'; // Let's see what time it is Down Under
const timeInBrisbane = utcToZonedTime(today, timeZone);

console.log(`
  Time in Munich: ${format(today, 'yyyy-MM-dd HH:mm:ss')}
  Time in Brisbane: ${format(timeInBrisbane, 'yyyy-MM-dd HH:mm:ss')}
`);

// Time in Munich: 2020-09-16 13:26:48
// Time in Brisbane: 2020-09-16 21:26:48

Conclusion

Date-fns is a great little library that puts a whole bunch of helper methods for working with dates and times in JavaScript at your finger tips. It is under active development and now that Moment.js has been put into maintenance mode, it makes it a great replacement for Moment.js.

I hope this article has given you enough understanding and inspiration to go check it out and to start using it in your own projects.