React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

date-fnsdatesformattingparsingtimelocaletree-shaking

date-fns - Modular, tree-shakable date utility library for formatting, parsing, and manipulation

Recipe

npm install date-fns
import { format, parseISO, addDays, differenceInDays } from "date-fns";
 
// Format a date
format(new Date(), "MMMM d, yyyy"); // "April 6, 2026"
 
// Parse ISO string
const date = parseISO("2026-04-06T12:00:00Z");
 
// Add days
const nextWeek = addDays(new Date(), 7);
 
// Difference between dates
const days = differenceInDays(new Date("2026-12-31"), new Date()); // days until end of year

When to reach for this: You need to format, parse, compare, or manipulate dates with a lightweight, tree-shakable library that uses native Date objects.

Working Example

// app/components/ActivityFeed.tsx
"use client";
import {
  format,
  formatDistanceToNow,
  isToday,
  isYesterday,
  isThisWeek,
  parseISO,
} from "date-fns";
 
interface Activity {
  id: string;
  action: string;
  timestamp: string; // ISO string
  user: string;
}
 
const ACTIVITIES: Activity[] = [
  { id: "1", action: "pushed to main", timestamp: new Date().toISOString(), user: "Alice" },
  { id: "2", action: "opened PR #42", timestamp: new Date(Date.now() - 3600000).toISOString(), user: "Bob" },
  { id: "3", action: "merged PR #41", timestamp: new Date(Date.now() - 86400000).toISOString(), user: "Carol" },
  { id: "4", action: "created issue", timestamp: new Date(Date.now() - 259200000).toISOString(), user: "Dave" },
  { id: "5", action: "deployed v2.1", timestamp: "2026-03-20T10:30:00Z", user: "Eve" },
];
 
function formatTimestamp(isoString: string): string {
  const date = parseISO(isoString);
 
  if (isToday(date)) {
    return formatDistanceToNow(date, { addSuffix: true }); // "2 hours ago"
  }
  if (isYesterday(date)) {
    return `Yesterday at ${format(date, "h:mm a")}`; // "Yesterday at 3:30 PM"
  }
  if (isThisWeek(date)) {
    return format(date, "EEEE 'at' h:mm a"); // "Monday at 10:30 AM"
  }
  return format(date, "MMM d, yyyy"); // "Mar 20, 2026"
}
 
export default function ActivityFeed() {
  return (
    <div className="max-w-lg mx-auto p-6">
      <h2 className="text-xl font-bold mb-4">Activity</h2>
      <ul className="space-y-3">
        {ACTIVITIES.map((activity) => (
          <li key={activity.id} className="flex justify-between items-center border-b pb-2">
            <div>
              <span className="font-medium">{activity.user}</span>{" "}
              <span className="text-gray-600">{activity.action}</span>
            </div>
            <time
              dateTime={activity.timestamp}
              className="text-sm text-gray-400 whitespace-nowrap"
            >
              {formatTimestamp(activity.timestamp)}
            </time>
          </li>
        ))}
      </ul>
    </div>
  );
}

What this demonstrates:

  • Smart timestamp formatting with relative time for recent events
  • parseISO for safe ISO string parsing
  • isToday, isYesterday, isThisWeek for conditional formatting
  • formatDistanceToNow for "X hours ago" style output
  • Semantic <time> element with dateTime attribute

Deep Dive

How It Works

  • date-fns operates on native JavaScript Date objects, not a custom wrapper class
  • Each function is a standalone ES module, enabling tree-shaking by bundlers (only imported functions are included in the bundle)
  • format uses Unicode Technical Standard #35 tokens: yyyy (year), MM (month), dd (day), HH (24h), hh (12h), mm (minutes), ss (seconds)
  • parseISO is preferred over new Date(string) because it handles ISO 8601 strings consistently across browsers
  • Comparison functions (isBefore, isAfter, isEqual) compare timestamps, not date strings
  • All functions are pure and do not mutate the input date

Variations

Date arithmetic:

import {
  addDays,
  addMonths,
  addHours,
  subWeeks,
  startOfMonth,
  endOfMonth,
  eachDayOfInterval,
} from "date-fns";
 
const tomorrow = addDays(new Date(), 1);
const nextMonth = addMonths(new Date(), 1);
const twoHoursLater = addHours(new Date(), 2);
const lastWeek = subWeeks(new Date(), 1);
 
// Get all days in current month (useful for calendars)
const monthStart = startOfMonth(new Date());
const monthEnd = endOfMonth(new Date());
const daysInMonth = eachDayOfInterval({ start: monthStart, end: monthEnd });

Locale support:

import { format, formatDistanceToNow } from "date-fns";
import { fr } from "date-fns/locale";
import { ja } from "date-fns/locale";
 
format(new Date(), "EEEE d MMMM yyyy", { locale: fr });
// "lundi 6 avril 2026"
 
formatDistanceToNow(new Date(Date.now() - 7200000), {
  addSuffix: true,
  locale: ja,
});
// "約2時間前"

Date range and comparison:

import {
  isWithinInterval,
  isBefore,
  isAfter,
  differenceInCalendarDays,
  differenceInBusinessDays,
  areIntervalsOverlapping,
} from "date-fns";
 
const eventStart = new Date("2026-04-10");
const eventEnd = new Date("2026-04-15");
 
// Check if a date is in range
isWithinInterval(new Date("2026-04-12"), {
  start: eventStart,
  end: eventEnd,
}); // true
 
// Business days between dates (excludes weekends)
differenceInBusinessDays(eventEnd, eventStart); // 3
 
// Check for overlapping intervals
areIntervalsOverlapping(
  { start: eventStart, end: eventEnd },
  { start: new Date("2026-04-13"), end: new Date("2026-04-20") }
); // true

Parsing custom date strings:

import { parse, isValid } from "date-fns";
 
const date = parse("04/06/2026", "MM/dd/yyyy", new Date());
console.log(isValid(date)); // true
 
const invalid = parse("not-a-date", "MM/dd/yyyy", new Date());
console.log(isValid(invalid)); // false

TypeScript Notes

  • All date-fns functions accept Date or number (timestamp) as date arguments
  • Return types are explicit: format returns string, addDays returns Date, differenceInDays returns number
  • Locale types are exported from date-fns/locale
  • No additional @types package needed; types are included in date-fns
import type { Locale } from "date-fns";
import { fr } from "date-fns/locale";
 
function formatDate(date: Date | number, locale: Locale = fr): string {
  return format(date, "PPP", { locale });
}

Gotchas

  • Timezone confusionDate objects are always in the local timezone. format outputs local time, not UTC. Fix: Use date-fns-tz for timezone-aware formatting, or use formatInTimeZone from date-fns-tz.

  • Format token case sensitivityMM is month, mm is minutes. DD is not valid (use dd). Fix: Refer to the format token table. Common tokens: yyyy-MM-dd HH:mm:ss.

  • parseISO vs new Datenew Date("2026-04-06") is parsed as UTC midnight, but displayed in local time, which can shift the date. Fix: Always use parseISO for ISO strings to get consistent behavior.

  • Mutable Date pitfall — While date-fns functions are pure, if you pass the same Date instance to multiple places after mutating it elsewhere, you get unexpected results. Fix: Create new Date instances or use the return values from date-fns functions.

  • Bundle size with locales — Importing all locales adds significant bundle size. Fix: Import only the locales you need: import { fr } from "date-fns/locale", not import * as locales from "date-fns/locale".

  • Server/client date mismatch — Server-rendered dates use the server's timezone, causing hydration mismatches. Fix: Format dates only on the client side, or pass pre-formatted strings from the server, or use suppressHydrationWarning on the <time> element.

Alternatives

LibraryBest ForTrade-off
date-fnsTree-shakable, functional APINo timezone support without date-fns-tz
dayjsMoment.js drop-in replacement (2KB)Plugin-based, mutable API
Temporal (proposal)Future native JS date APINot yet available in all runtimes
LuxonFull timezone and i18n supportLarger bundle (around 20KB)
Moment.jsLegacy projects onlyDeprecated, 67KB minified, mutable

FAQs

Why should I use parseISO instead of new Date(string)?
  • new Date("2026-04-06") is parsed as UTC midnight but displayed in local time, which can shift the date by a day
  • parseISO handles ISO 8601 strings consistently across all browsers
  • It returns an invalid date for non-ISO strings rather than guessing
How do I format a date as "2 hours ago" or "3 days ago"?
import { formatDistanceToNow } from "date-fns";
 
formatDistanceToNow(someDate, { addSuffix: true });
// "2 hours ago" or "in 3 days"

The addSuffix option prepends "in" or appends "ago" automatically.

Gotcha: Why does MM give me months but mm gives me minutes?
  • Format tokens are case-sensitive in date-fns (Unicode TR#35 standard)
  • MM = month (01-12), mm = minutes (00-59)
  • DD is not valid -- use dd for day of month
  • Common safe pattern: yyyy-MM-dd HH:mm:ss
How do I generate all days in a month for a calendar component?
import { startOfMonth, endOfMonth, eachDayOfInterval } from "date-fns";
 
const start = startOfMonth(new Date());
const end = endOfMonth(new Date());
const days = eachDayOfInterval({ start, end });
How do I add locale support for non-English date formatting?
import { format } from "date-fns";
import { fr } from "date-fns/locale";
 
format(new Date(), "EEEE d MMMM yyyy", { locale: fr });
// "lundi 6 avril 2026"

Import only the locales you need to keep bundle size small.

What is the difference between differenceInDays and differenceInCalendarDays?
  • differenceInDays calculates based on full 24-hour periods
  • differenceInCalendarDays counts calendar day boundaries crossed
  • Example: 11:00 PM to 1:00 AM is 0 with differenceInDays but 1 with differenceInCalendarDays
How do I check if a date falls within a range?
import { isWithinInterval } from "date-fns";
 
isWithinInterval(new Date("2026-04-12"), {
  start: new Date("2026-04-10"),
  end: new Date("2026-04-15"),
}); // true
Gotcha: Why do server-rendered dates cause hydration mismatches?
  • The server formats dates in the server's timezone
  • The client re-renders in the user's local timezone, producing different text
  • Fix: format dates only on the client, pass pre-formatted strings from the server, or use suppressHydrationWarning on the <time> element
How do I handle timezones with date-fns?
  • date-fns itself has no timezone support -- Date objects use the local timezone
  • Install date-fns-tz for timezone-aware formatting
  • Use formatInTimeZone to display dates in a specific timezone
  • Always store and transmit dates as UTC ISO strings
How do I type a function that accepts both Date and timestamp number in TypeScript?
import type { Locale } from "date-fns";
import { format } from "date-fns";
 
function formatDate(
  date: Date | number,
  pattern: string = "PPP",
  locale?: Locale
): string {
  return format(date, pattern, { locale });
}

All date-fns functions accept Date | number as date arguments.

How do I parse a custom date string format?
import { parse, isValid } from "date-fns";
 
const date = parse("04/06/2026", "MM/dd/yyyy", new Date());
console.log(isValid(date)); // true

Always validate with isValid since parse returns an invalid Date for bad input.

Does date-fns tree-shake effectively and how do I verify?
  • date-fns uses ES module exports so bundlers can eliminate unused functions
  • Import individual functions: import { format } from "date-fns" includes only format and its dependencies
  • Avoid import * as dateFns from "date-fns" which defeats tree-shaking
  • Check your bundle analyzer to confirm only imported functions are included
  • React Hooks — Custom hooks for date formatting
  • Lodash — Utility functions that complement date-fns
  • TanStack Query — Cache date-dependent API responses