Skip to content

Migration Guide

This guide helps you migrate from various date libraries to useTemporal, or switch between different useTemporal adapters.

Quick Migration Reference

FromToComplexityKey Changes
Native DateuseTemporal (native)SimpleUse Period-based API
date-fnsuseTemporal + date-fnsSimpleKeep utilities, add structure
date-fns-tzuseTemporal + date-fns-tzSimpleKeep timezone support
LuxonuseTemporal + luxonSimpleKeep timezone features
Temporal APIuseTemporal + temporalSimpleFuture-proof solution

From Vanilla JavaScript

Basic Date Operations

javascript
// Before: Vanilla JavaScript
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
const tomorrow = new Date(now)
tomorrow.setDate(tomorrow.getDate() + 1)

// After: useTemporal
import { createTemporal, period, next } from '@allystudio/usetemporal'

const temporal = createTemporal()
const month = temporal.period( new Date(), 'month')
const startOfMonth = month.start
const endOfMonth = new Date(month.end.getTime() - 1) // end is exclusive
const tomorrow = next(temporal.adapter, period(temporal, new Date(), 'day'))

Date Comparisons

javascript
// Before: Vanilla JavaScript
function isSameDay(date1, date2) {
  return date1.getFullYear() === date2.getFullYear() &&
         date1.getMonth() === date2.getMonth() &&
         date1.getDate() === date2.getDate()
}

function isDateInRange(date, start, end) {
  return date >= start && date <= end
}

// After: useTemporal
import { isSame, contains } from '@allystudio/usetemporal'

function isSameDay(temporal, date1, date2) {
  return isSame(temporal.adapter, date1, date2, 'day')
}

function isDateInRange(temporal, date, period) {
  return contains(temporal.adapter, period, date)
}

Calendar Generation

javascript
// Before: Complex vanilla JavaScript
function getMonthDays(year, month) {
  const days = []
  const date = new Date(year, month, 1)
  while (date.getMonth() === month) {
    days.push(new Date(date))
    date.setDate(date.getDate() + 1)
  }
  return days
}

// After: Simple with useTemporal
import { divide } from '@allystudio/usetemporal'

function getMonthDays(temporal, date) {
  const month = temporal.period( date, 'month')
  return divide(temporal.adapter, month, 'day')
}

From date-fns

Basic Operations

javascript
// Before: date-fns
import { 
  startOfMonth, 
  endOfMonth, 
  addWeeks, 
  format,
  eachDayOfInterval 
} from 'date-fns'

const monthStart = startOfMonth(new Date())
const monthEnd = endOfMonth(new Date())
const nextWeek = addWeeks(new Date(), 1)
const days = eachDayOfInterval({ start: monthStart, end: monthEnd })
const formatted = format(new Date(), 'yyyy-MM-dd')

// After: useTemporal with date-fns adapter
import { createTemporal, period, go, divide } from '@allystudio/usetemporal'
import { createDateFnsAdapter } from '@allystudio/usetemporal-adapter-date-fns'
import { format } from 'date-fns' // Keep using for formatting!

const temporal = createTemporal({ adapter: createDateFnsAdapter() })
const month = temporal.period( new Date(), 'month')
const nextWeek = go(temporal.adapter, period(temporal, new Date(), 'day'), 1, 'week')
const days = divide(temporal.adapter, month, 'day')
const formatted = format(new Date(), 'yyyy-MM-dd') // Still use date-fns!

Locale Operations

javascript
// Before: date-fns with locale
import { format } from 'date-fns'
import { es, fr } from 'date-fns/locale'

const spanish = format(new Date(), 'MMMM', { locale: es })
const french = format(new Date(), 'MMMM', { locale: fr })

// After: Same! Keep using date-fns for formatting
const period = temporal.period( new Date(), 'month')
const spanish = format(period.start, 'MMMM', { locale: es })
const french = format(period.start, 'MMMM', { locale: fr })

Advanced Patterns

javascript
// Before: date-fns functions
import { 
  eachWeekOfInterval,
  eachMonthOfInterval,
  isWithinInterval,
  areIntervalsOverlapping 
} from 'date-fns'

const year = { start: startOfYear(date), end: endOfYear(date) }
const months = eachMonthOfInterval(year)
const weeks = eachWeekOfInterval(year)

// After: useTemporal patterns
const year = temporal.period( date, 'year')
const months = divide(temporal.adapter, year, 'month')
const weeks = divide(temporal.adapter, year, 'week')

// Still use date-fns for specific utilities
import { isWithinInterval } from 'date-fns'
const inRange = isWithinInterval(date, { 
  start: period.start, 
  end: period.end 
})

From Luxon

Basic Migration

javascript
// Before: Luxon
import { DateTime } from 'luxon'

const now = DateTime.now()
const startOfMonth = now.startOf('month')
const inTokyo = now.setZone('Asia/Tokyo')
const formatted = now.toFormat('yyyy-MM-dd')

// After: useTemporal with Luxon adapter
import { createTemporal, period } from '@allystudio/usetemporal'
import { createLuxonAdapter } from '@allystudio/usetemporal-adapter-luxon'
import { DateTime } from 'luxon' // Keep for formatting!

const temporal = createTemporal({ 
  adapter: createLuxonAdapter({ zone: 'local' }) 
})
const now = temporal.period( new Date(), 'day')
const month = temporal.period( new Date(), 'month')

// For timezone operations, create new temporal
const tokyoTemporal = createTemporal({
  adapter: createLuxonAdapter({ zone: 'Asia/Tokyo' })
})
const inTokyo = period(tokyoTemporal, new Date(), 'day')

// Still use Luxon for formatting
const formatted = DateTime.fromJSDate(now.start).toFormat('yyyy-MM-dd')

Timezone Operations

javascript
// Before: Luxon timezone handling
const meeting = DateTime.fromObject({
  year: 2025,
  month: 7,
  day: 25,
  hour: 14,
  zone: 'America/New_York'
})
const londonTime = meeting.setZone('Europe/London')

// After: Multiple temporal instances
const nyTemporal = createTemporal({
  adapter: createLuxonAdapter({ zone: 'America/New_York' })
})
const londonTemporal = createTemporal({
  adapter: createLuxonAdapter({ zone: 'Europe/London' })
})

// Same moment in different timezones
const sameMoment = new Date('2025-07-25T14:00:00-04:00')
const nyMeeting = period(nyTemporal, sameMoment, 'hour')
const londonMeeting = period(londonTemporal, sameMoment, 'hour')

From date-fns-tz

Basic Migration

javascript
// Before: date-fns-tz
import { format, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'

const nyDate = utcToZonedTime(new Date(), 'America/New_York')
const utcDate = zonedTimeToUtc(nyDate, 'America/New_York')
const formatted = format(nyDate, 'yyyy-MM-dd HH:mm:ss zzz', {
  timeZone: 'America/New_York'
})

// After: useTemporal with date-fns-tz adapter
import { createTemporal, period } from '@allystudio/usetemporal'
import { createDateFnsTzAdapter } from '@allystudio/usetemporal/date-fns-tz'
import { format } from 'date-fns-tz' // Keep for formatting!

const temporal = createTemporal({
  adapter: createDateFnsTzAdapter({ timezone: 'America/New_York' })
})

const nyPeriod = temporal.period( new Date(), 'hour')
// The period is already in NY timezone

// Still use date-fns-tz for formatting
const formatted = format(nyPeriod.start, 'yyyy-MM-dd HH:mm:ss zzz', {
  timeZone: 'America/New_York'
})

Working with Multiple Timezones

javascript
// Before: Manual timezone conversions
import { utcToZonedTime, format } from 'date-fns-tz'

function showMeetingTimes(utcDate) {
  const zones = ['America/New_York', 'Europe/London', 'Asia/Tokyo']
  return zones.map(zone => ({
    zone,
    time: format(utcToZonedTime(utcDate, zone), 'PPpp', { timeZone: zone })
  }))
}

// After: Cleaner with multiple temporal instances
function showMeetingTimes(date) {
  const zones = ['America/New_York', 'Europe/London', 'Asia/Tokyo']
  return zones.map(zone => {
    const temporal = createTemporal({
      adapter: createDateFnsTzAdapter({ timezone: zone })
    })
    const period = temporal.period( date, 'hour')
    return {
      zone,
      time: format(period.start, 'PPpp', { timeZone: zone })
    }
  })
}

Switching Between Adapters

Runtime Adapter Switching

typescript
// Start with native for performance
let temporal = createTemporal()

// Switch to Luxon when timezone needed
function enableTimezoneSupport(timezone: string) {
  temporal = createTemporal({
    adapter: createLuxonAdapter({ zone: timezone })
  })
}

// All code continues to work!
const period = temporal.period( new Date(), 'day')

Gradual Migration Strategy

typescript
// Phase 1: Use adapter matching your current library
const temporal = createTemporal({
  adapter: createDateFnsAdapter() // If using date-fns
})

// Phase 2: Migrate code to useTemporal patterns
// Old date library code and new useTemporal code coexist

// Phase 3: Switch to more modern adapter
const temporal = createTemporal({
  adapter: createDateFnsAdapter() // Smaller, maintained
})

// Phase 4: Optimize - use native where possible
const temporal = createTemporal() // Zero dependencies!

Feature-Based Selection

typescript
// Create different temporal instances for different features
const adapters = {
  // Fast local operations
  local: createTemporal(),
  
  // Formatting and locales
  formatting: createTemporal({
    adapter: createDateFnsAdapter()
  }),
  
  // Timezone operations
  timezone: createTemporal({
    adapter: createLuxonAdapter()
  })
}

// Use appropriate adapter for each operation
function formatDate(date: Date, locale: Locale) {
  const period = period(adapters.formatting, date, 'day')
  return format(period.start, 'PPP', { locale })
}

function convertTimezone(date: Date, from: string, to: string) {
  const fromTemporal = createTemporal({
    adapter: createLuxonAdapter({ zone: from })
  })
  const toTemporal = createTemporal({
    adapter: createLuxonAdapter({ zone: to })
  })
  // ... conversion logic
}

Common Migration Patterns

Replace Loops with divide()

javascript
// Before: Manual date loops
const days = []
const current = new Date(startDate)
while (current <= endDate) {
  days.push(new Date(current))
  current.setDate(current.getDate() + 1)
}

// After: Clean divide pattern
const period = { start: startDate, end: endDate }
const days = divide(temporal.adapter, period, 'day')

Replace Complex Calculations

javascript
// Before: Complex month calculations
function getWeeksInMonth(year, month) {
  const firstDay = new Date(year, month, 1).getDay()
  const daysInMonth = new Date(year, month + 1, 0).getDate()
  return Math.ceil((firstDay + daysInMonth) / 7)
}

// After: Simple divide
function getWeeksInMonth(temporal, date) {
  const month = temporal.period( date, 'month')
  return divide(temporal.adapter, month, 'week').length
}

Simplify Relative Date Logic

javascript
// Before: Relative date calculations
function getLastMonday() {
  const today = new Date()
  const day = today.getDay()
  const diff = today.getDate() - day + (day === 0 ? -6 : 1)
  return new Date(today.setDate(diff))
}

// After: Clear intent
function getThisMonday(temporal) {
  const week = temporal.period( new Date(), 'week')
  return week.start // Weeks start on configured day
}

Performance Migration Tips

1. Benchmark Critical Paths

javascript
// Measure performance impact
console.time('operation')
// Your date operations
console.timeEnd('operation')

// Compare adapters
const adapters = [
  { name: 'Native', temporal: createTemporal() },
  { name: 'date-fns', temporal: createTemporal({ adapter: createDateFnsAdapter() }) },
  // ... other adapters
]

adapters.forEach(({ name, temporal }) => {
  console.time(name)
  // Run same operations
  for (let i = 0; i < 1000; i++) {
    const period = temporal.period( new Date(), 'month')
    const days = divide(temporal.adapter, period, 'day')
  }
  console.timeEnd(name)
})

2. Lazy Load Adapters

javascript
// Load adapters only when needed
async function getTemporalWithTimezone() {
  const { createLuxonAdapter } = await import('@allystudio/usetemporal-adapter-luxon')
  return createTemporal({ adapter: createLuxonAdapter() })
}

3. Use Native for Hot Paths

javascript
// Use native adapter for performance-critical code
const fastTemporal = createTemporal() // Native

// Use feature-rich adapter only when needed
const richTemporal = await loadRichAdapter()

Next Steps

Released under the MIT License.