How to Migrate from Moment.js to date-fns
·PkgPulse Team
TL;DR
Migrating from Moment.js to date-fns is mostly a find-and-replace. The main mental model shift: Moment.js is OOP (chained methods on a Moment object), date-fns is functional (pure functions that take a Date). You'll write format(new Date(), 'yyyy-MM-dd') instead of moment().format('YYYY-MM-DD'). One gotcha: date-fns uses different format token casing — yyyy not YYYY, dd not DD.
Key Takeaways
- Mental model shift: OOP chains → pure functions
- Format tokens differ:
YYYY→yyyy,DD→dd,HH→HH(same),mm→mm(same) - Moment objects → native Date: date-fns works with JavaScript's built-in
Date - All functions are pure — no mutation, no hidden timezone state
- Bundle impact: Moment.js 72KB → date-fns only what you import (~2-5KB typical)
The API Mapping
Basic Formatting
// Before (Moment.js):
moment().format('YYYY-MM-DD'); // '2026-03-08'
moment().format('MMMM Do YYYY'); // 'March 8th 2026'
moment('2026-03-08').format('dddd'); // 'Sunday'
// After (date-fns):
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd'); // '2026-03-08'
format(new Date(), 'MMMM do yyyy'); // 'March 8th 2026'
format(new Date('2026-03-08'), 'EEEE'); // 'Sunday'
Format Token Differences
⚠️ The most common source of bugs during migration:
Moment.js → date-fns Meaning
─────────────────────────────────────────────────
YYYY → yyyy Full year (2026)
YY → yy Short year (26)
DD → dd Day of month (08)
D → d Day of month (8, no pad)
ddd → EEE Short day name (Sun)
dddd → EEEE Full day name (Sunday)
A → a AM/PM (uppercase result: AM)
a → aaa am/pm (lowercase)
Z → xxx Timezone offset (+05:30)
x → T Unix milliseconds (use getTime())
Unchanged:
MM → MM Month number
MMM → MMM Short month (Mar)
MMMM → MMMM Full month (March)
HH → HH 24hr hours
hh → hh 12hr hours
mm → mm Minutes
ss → ss Seconds
Parsing
// Before (Moment.js):
moment('2026-03-08'); // Parse ISO string
moment('03/08/2026', 'MM/DD/YYYY'); // Parse with format
moment(1709856000000); // Parse Unix milliseconds
moment.unix(1709856); // Parse Unix seconds
// After (date-fns):
import { parseISO, parse, fromUnixTime } from 'date-fns';
parseISO('2026-03-08'); // Parse ISO string
parse('03/08/2026', 'MM/dd/yyyy', new Date()); // Parse with format
new Date(1709856000000); // Native Date for milliseconds
fromUnixTime(1709856); // Parse Unix seconds
Date Arithmetic
// Before (Moment.js — mutable!):
const date = moment();
date.add(7, 'days'); // Mutates date!
date.subtract(1, 'month'); // Mutates date!
date.startOf('month'); // Mutates date!
date.endOf('week'); // Mutates date!
// After (date-fns — immutable, always returns new Date):
import { addDays, subMonths, startOfMonth, endOfWeek } from 'date-fns';
const date = new Date();
const plusWeek = addDays(date, 7); // New Date, original unchanged
const minusMonth = subMonths(date, 1); // New Date
const monthStart = startOfMonth(date); // New Date
const weekEnd = endOfWeek(date); // New Date
Comparison
// Before (Moment.js):
moment('2026-03-08').isBefore(moment('2026-06-01'));
moment('2026-03-08').isAfter(moment('2026-01-01'));
moment('2026-03-08').isSame(moment('2026-03-08'), 'day');
moment('2026-03-08').isBetween('2026-01-01', '2026-12-31');
moment('2026-03-08').diff(moment('2026-01-01'), 'days');
// After (date-fns):
import { isBefore, isAfter, isSameDay, isWithinInterval, differenceInDays } from 'date-fns';
isBefore(new Date('2026-03-08'), new Date('2026-06-01'));
isAfter(new Date('2026-03-08'), new Date('2026-01-01'));
isSameDay(new Date('2026-03-08'), new Date('2026-03-08'));
isWithinInterval(new Date('2026-03-08'), {
start: new Date('2026-01-01'),
end: new Date('2026-12-31'),
});
differenceInDays(new Date('2026-03-08'), new Date('2026-01-01')); // 66
Relative Time
// Before (Moment.js):
moment('2026-01-01').fromNow(); // "2 months ago"
moment('2027-01-01').fromNow(); // "in 10 months"
moment().from(moment('2026-01-01')); // "in a few seconds"
// After (date-fns):
import { formatDistanceToNow, formatDistance } from 'date-fns';
formatDistanceToNow(new Date('2026-01-01'), { addSuffix: true }); // "2 months ago"
formatDistanceToNow(new Date('2027-01-01'), { addSuffix: true }); // "in 10 months"
formatDistance(new Date(), new Date('2026-01-01')); // "about 2 months"
Timezone Migration
// Moment.js: moment-timezone plugin
import moment from 'moment-timezone';
moment.tz('2026-03-08T14:00:00', 'America/New_York').format();
// date-fns: date-fns-tz package
import { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz';
// Format in a specific timezone
formatInTimeZone(new Date('2026-03-08T14:00:00Z'), 'America/New_York', 'yyyy-MM-dd HH:mm zzz');
// '2026-03-08 09:00 EST'
// Convert UTC to local timezone object
const nyDate = toZonedTime(new Date('2026-03-08T14:00:00Z'), 'America/New_York');
// Convert local timezone to UTC
const utcDate = fromZonedTime(new Date('2026-03-08T09:00:00'), 'America/New_York');
i18n Migration
// Moment.js: import and set locale globally
import 'moment/locale/fr';
moment.locale('fr');
moment().format('MMMM'); // 'mars'
// date-fns: import locale per function call (tree-shakeable)
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
format(new Date(), 'MMMM', { locale: fr }); // 'mars'
formatDistanceToNow(new Date(), { locale: fr, addSuffix: true }); // 'il y a quelques secondes'
Codemod: Automate the Migration
# @date-fns/upgrade codemod (community tool)
npx @date-fns/upgrade
# Handles:
# - moment() → new Date()
# - .format() → format() with token conversion
# - .add() / .subtract() → addDays/subDays etc.
# - Import replacements
# Always review the diff manually — format tokens need verification
git diff src/
Compare date library package health on PkgPulse.
See the live comparison
View dayjs vs. date fns on PkgPulse →