Reactive Time Units
useTemporal leverages @vue/reactivity (not the Vue framework) to provide reactive time units that automatically update when the browsing period changes.
How It Works
Core Reactivity
useTemporal uses Vue's standalone reactivity system:
import { ref, computed, reactive } from '@vue/reactivity'
// Inside createTemporal
const temporal = {
browsing: ref(initialPeriod),
now: ref(currentTimePeriod),
// ...
}This reactivity system is:
- Framework agnostic - Works with any UI framework
- Tree-shakeable - Only includes what you use
- Efficient - Tracks dependencies automatically
Reactive Periods
The usePeriod composable creates reactive periods:
const month = usePeriod(temporal, 'month')
// month is a ComputedRef<Period>
// Automatically updates when browsing changes
temporal.browsing.value = next(temporal.adapter, temporal.browsing.value)
// month.value now reflects the new monthIntegration with Frameworks
Vue 3
Works seamlessly with Vue's reactivity:
<script setup>
import { usePeriod, divide } from '@allystudio/usetemporal'
import { computed } from 'vue'
const props = defineProps(['temporal'])
const month = usePeriod(props.temporal, 'month')
const days = computed(() => divide(props.temporal.adapter, month.value, 'day'))
</script>
<template>
<div>
<h2>{{ month.value.date.toLocaleDateString('en', { month: 'long' }) }}</h2>
<div v-for="day in days" :key="day.date.toISOString()">
{{ day.date.getDate() }}
</div>
</div>
</template>React
Use with React's state management:
import { useEffect, useState, useSyncExternalStore } from 'react'
import { usePeriod, divide } from '@allystudio/usetemporal'
function useReactivePeriod(temporal, unit) {
return useSyncExternalStore(
(callback) => {
// Subscribe to changes
const stop = temporal.browsing.watch(callback)
return stop
},
() => usePeriod(temporal, unit).value
)
}
function Calendar({ temporal }) {
const month = useReactivePeriod(temporal, 'month')
const [days, setDays] = useState([])
useEffect(() => {
setDays(divide(temporal.adapter, month, 'day'))
}, [month, temporal])
return (
<div>
{days.map(day => (
<div key={day.date.toISOString()}>
{day.date.getDate()}
</div>
))}
</div>
)
}Svelte
Integration with Svelte stores:
import { writable, derived } from 'svelte/store'
import { usePeriod, divide } from '@allystudio/usetemporal'
// Create a Svelte store from temporal
function createTemporalStore(temporal) {
const { subscribe, set } = writable(temporal.browsing.value)
// Watch for changes
temporal.browsing.watch((newValue) => {
set(newValue)
})
return {
subscribe,
next: () => temporal.browsing.value = next(temporal.adapter, temporal.browsing.value),
previous: () => temporal.browsing.value = previous(temporal.adapter, temporal.browsing.value)
}
}
// In component
const browsing = createTemporalStore(temporal)
const month = derived(browsing, $browsing =>
usePeriod(temporal, 'month').value
)Vanilla JavaScript
Use without any framework:
import { createTemporal, usePeriod, divide } from '@allystudio/usetemporal'
const temporal = createTemporal({ date: new Date() })
const month = usePeriod(temporal, 'month')
// Manual subscription
let unwatch = month.watch((newMonth) => {
console.log('Month changed:', newMonth)
updateCalendarUI(newMonth)
})
// Update browsing
document.getElementById('next').addEventListener('click', () => {
temporal.browsing.value = next(temporal.adapter, month.value)
})
// Cleanup when done
// unwatch()Reactive Patterns
Computed Chains
Build reactive computation chains:
const year = usePeriod(temporal, 'year')
const months = computed(() => divide(temporal.adapter, year.value, 'month'))
const currentMonth = computed(() =>
months.value.find(m => isSame(temporal.adapter, m, temporal.now.value, 'month'))
)
const days = computed(() =>
currentMonth.value ? divide(temporal.adapter, currentMonth.value, 'day') : []
)Reactive Filtering
Filter periods reactively:
const month = usePeriod(temporal, 'month')
const allDays = computed(() => divide(temporal.adapter, month.value, 'day'))
const weekdays = computed(() => allDays.value.filter(isWeekday))
const weekends = computed(() => allDays.value.filter(isWeekend))
const today = computed(() =>
allDays.value.find(day => isToday(day, temporal))
)Side Effects
Handle side effects with watchers:
import { watch } from '@vue/reactivity'
const month = usePeriod(temporal, 'month')
// Watch for changes
watch(month, (newMonth, oldMonth) => {
console.log('Month changed from', oldMonth, 'to', newMonth)
// Fetch data for new month
fetchEventsForMonth(newMonth)
// Update URL
updateURLParams({ month: newMonth.date.toISOString() })
})Performance Considerations
Memoization
Reactive computations are automatically memoized:
const month = usePeriod(temporal, 'month')
const days = computed(() => {
console.log('Computing days...') // Only runs when month changes
return divide(temporal.adapter, month.value, 'day')
})
// Access multiple times - computation only runs once
console.log(days.value.length)
console.log(days.value[0])Lazy Evaluation
Computations are lazy - only run when accessed:
const expensiveComputation = computed(() => {
console.log('This only runs if accessed')
return divide(temporal.adapter, year.value, 'hour') // 8760+ periods
})
// Computation hasn't run yet
if (showHourlyView) {
console.log(expensiveComputation.value) // Now it runs
}Cleanup
Always clean up watchers to prevent memory leaks:
const stopWatching = watch(temporal.browsing, (newValue) => {
// Handle changes
})
// When component unmounts
onUnmounted(() => {
stopWatching()
})Advanced Patterns
Custom Reactive Composables
Create your own reactive time utilities:
function useTimeRange(temporal, startDate, endDate) {
return computed(() => {
const periods = []
let current = temporal.period( startDate, 'day')
const end = temporal.period( endDate, 'day')
while (current.start <= end.start) {
periods.push(current)
current = next(temporal.adapter, current)
}
return periods
})
}
// Usage
const range = useTimeRange(
temporal,
new Date('2024-01-01'),
new Date('2024-01-31')
)Reactive Aggregations
Build reactive statistics:
function useMonthStats(temporal) {
const month = usePeriod(temporal, 'month')
return computed(() => {
const days = divide(temporal.adapter, month.value, 'day')
return {
totalDays: days.length,
weekdays: days.filter(isWeekday).length,
weekends: days.filter(isWeekend).length,
weeks: Math.ceil(days.length / 7),
firstDay: days[0],
lastDay: days[days.length - 1]
}
})
}Debugging Reactive Time
Track Dependencies
See what triggers updates:
import { onTrack, onTrigger } from '@vue/reactivity'
const month = computed(() => {
return usePeriod(temporal, 'month').value
}, {
onTrack(e) {
console.log('Tracking:', e)
},
onTrigger(e) {
console.log('Triggered by:', e)
}
})Debug Subscriptions
Monitor active subscriptions:
let subscriptionCount = 0
function debugWatch(source, callback) {
subscriptionCount++
console.log(`Active subscriptions: ${subscriptionCount}`)
const stop = watch(source, callback)
return () => {
subscriptionCount--
console.log(`Active subscriptions: ${subscriptionCount}`)
stop()
}
}See Also
- Framework Agnostic - How it works without Vue
- usePeriod - Main reactive composable
- Vue Integration - Vue examples
- React Integration - React examples