Skip to content

Commit 1f8943e

Browse files
committed
refactor(useDate): address inspection findings
- Enhance SSR/hydration docs with warning callout and code examples - Add type-level tests verifying overload safety - Replace getCurrentInstance() with instanceExists() utility - Minor import ordering cleanup
1 parent 784900d commit 1f8943e

File tree

6 files changed

+68
-25
lines changed

6 files changed

+68
-25
lines changed

apps/docs/src/pages/composables/plugins/use-date.md

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -368,15 +368,40 @@ app.use(createDatePlugin({
368368

369369
- **parse() format parameter**: The `parse()` method's format parameter is currently ignored. The Temporal API doesn't provide built-in format parsing. The method delegates to `date()` which handles ISO 8601 strings. For custom format parsing, use a library like date-fns or luxon with a custom adapter.
370370

371-
- **SSR Behavior**: When `adapter.date()` is called without arguments:
372-
- Browser: Returns current time via `Temporal.Now.plainDateTimeISO()`
373-
- Server: Returns epoch (1970-01-01T00:00:00) for deterministic rendering
371+
### SSR and Hydration
374372

375-
This is intentional to prevent hydration mismatches. For SSR apps needing current time, pass `Date.now()` explicitly and handle hydration via `<ClientOnly>` (Nuxt) or `v-if` + `onMounted` pattern.
373+
> [!WARNING]
374+
> Date formatting can cause hydration mismatches in SSR applications. Server and client environments may produce different formatted output due to timezone differences.
376375

377-
- **Timezone-dependent formatting**: `Intl.DateTimeFormat` uses the system timezone. Server environments (often UTC) and client browsers (user's local timezone) may produce different formatted strings, causing hydration mismatches.
376+
**SSR Behavior for `adapter.date()`:**
377+
- Browser: Returns current time via `Temporal.Now.plainDateTimeISO()`
378+
- Server: Returns epoch (1970-01-01T00:00:00) for deterministic rendering
378379

379-
**Solutions:**
380-
- Set `TZ=UTC` environment variable on your server to match a consistent baseline
381-
- Wrap formatted date output in `<ClientOnly>` (Nuxt) or render only after `onMounted`
382-
- For critical date displays, serialize dates as ISO strings and format client-side only
380+
This is intentional to prevent hydration mismatches. For SSR apps needing current time, pass `Date.now()` explicitly.
381+
382+
**Timezone-dependent formatting:** `Intl.DateTimeFormat` uses the system timezone. Server environments (often UTC) and client browsers (user's local timezone) produce different formatted strings.
383+
384+
**Solutions:**
385+
1. **Nuxt/SSR:** Wrap formatted dates in `<ClientOnly>`:
386+
```vue
387+
<ClientOnly>
388+
<span>{{ adapter.format(date, 'fullDate') }}</span>
389+
</ClientOnly>
390+
```
391+
392+
2. **Vue SSR:** Defer formatting until after hydration:
393+
```vue
394+
<script setup lang="ts">
395+
const { adapter } = useDate()
396+
const isMounted = ref(false)
397+
const date = adapter.date('2024-06-15T10:30:00')
398+
399+
onMounted(() => { isMounted.value = true })
400+
401+
const formatted = computed(() =>
402+
isMounted.value ? adapter.format(date, 'fullDate') : date?.toString()
403+
)
404+
</script>
405+
```
406+
407+
3. **Server timezone:** Set `TZ=UTC` environment variable on your server for consistent baseline

packages/0/src/composables/useDate/adapters/v0.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
* @see https://github.com/dmtrKovalenko/date-io
1414
*/
1515

16-
// Polyfill
17-
import { Temporal } from '@js-temporal/polyfill'
18-
1916
// Constants
2017
import { IN_BROWSER } from '#v0/constants/globals'
18+
// Polyfill
19+
import { Temporal } from '@js-temporal/polyfill'
2120

2221
// Utilities
2322
import { isNull, isNullOrUndefined, isNumber, isString } from '#v0/utilities'

packages/0/src/composables/useDate/index.bench.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Temporal } from '@js-temporal/polyfill'
22
import { bench, describe } from 'vitest'
3+
4+
// Adapters
35
import { Vuetify0DateAdapter } from './adapters/v0'
46

57
describe('useDate benchmarks', () => {

packages/0/src/composables/useDate/index.ssr.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ vi.mock('#v0/constants/globals', () => ({
1111
IN_BROWSER: false,
1212
}))
1313

14+
// Adapters
1415
// Import after mock is set up
1516
import { Vuetify0DateAdapter } from './adapters/v0'
17+
1618
import { useDate, createDateFallback } from './index'
1719

1820
describe('useDate SSR', () => {
@@ -56,7 +58,7 @@ describe('useDate SSR', () => {
5658
})
5759
})
5860

59-
describe('Vuetify0DateAdapter in SSR mode', () => {
61+
describe('vuetify0DateAdapter in SSR mode', () => {
6062
it('should return epoch when date() called without arguments', () => {
6163
const adapter = new Vuetify0DateAdapter('en-US')
6264
const date = adapter.date()

packages/0/src/composables/useDate/index.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { describe, it, expect, beforeEach, expectTypeOf } from 'vitest'
21
import { Temporal } from '@js-temporal/polyfill'
3-
import { createDate, createDateContext, createDateFallback, createDatePlugin, useDate } from './index'
2+
import { describe, it, expect, beforeEach, expectTypeOf } from 'vitest'
3+
4+
// Adapters
45
import { Vuetify0DateAdapter } from './adapters/v0'
6+
7+
// Types
58
import type { DateAdapter, DateContext } from './index'
69

10+
import { createDate, createDateContext, createDateFallback, createDatePlugin, useDate } from './index'
11+
712
describe('useDate', () => {
8-
describe('Vuetify0DateAdapter', () => {
13+
describe('vuetify0DateAdapter', () => {
914
let adapter: Vuetify0DateAdapter
1015

1116
beforeEach(() => {
@@ -822,7 +827,7 @@ describe('useDate', () => {
822827
})
823828
})
824829

825-
describe('SSR behavior', () => {
830+
describe('sSR behavior', () => {
826831
it('should return deterministic date for explicit null/undefined input', () => {
827832
// When calling date() without arguments in SSR, the implementation
828833
// returns epoch (1970-01-01) for deterministic rendering.
@@ -1479,6 +1484,9 @@ describe('useDate', () => {
14791484
// useDate returns default type
14801485
const usedCtx = useDate()
14811486
expectTypeOf(usedCtx.adapter.date()).toEqualTypeOf<Temporal.PlainDateTime | null>()
1487+
1488+
// Runtime assertion to satisfy vitest/expect-expect rule
1489+
expect(defaultCtx.adapter).toBeDefined()
14821490
})
14831491

14841492
// Type-level tests: these verify TypeScript rejects invalid calls
@@ -1492,6 +1500,12 @@ describe('useDate', () => {
14921500

14931501
// @ts-expect-error - T provided without adapter should fail
14941502
createDatePlugin<Date>()
1503+
1504+
// @ts-expect-error - useDate<T>() without namespace should fail (second overload requires string)
1505+
useDate<Date>()
1506+
1507+
// Runtime assertion to satisfy vitest/expect-expect rule
1508+
expect(true).toBe(true)
14951509
})
14961510
})
14971511
})

packages/0/src/composables/useDate/index.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@
3737
* ```
3838
*/
3939

40+
// Foundational
41+
import { createContext, useContext } from '#v0/composables/createContext'
4042
// Factories
4143
import { createPlugin } from '#v0/composables/createPlugin'
4244
import { createTrinity } from '#v0/composables/createTrinity'
43-
import { createContext, useContext } from '#v0/composables/createContext'
4445

4546
// Composables
4647
import { useLocale } from '#v0/composables/useLocale'
@@ -49,14 +50,14 @@ import { useLocale } from '#v0/composables/useLocale'
4950
import { Vuetify0DateAdapter } from '#v0/composables/useDate/adapters'
5051

5152
// Utilities
52-
import { getCurrentInstance, computed, watchEffect, onScopeDispose } from 'vue'
53-
import { isNullOrUndefined } from '#v0/utilities'
53+
import { instanceExists, isNullOrUndefined } from '#v0/utilities'
54+
import { computed, watchEffect, onScopeDispose } from 'vue'
5455

5556
// Types
56-
import type { DateAdapter } from '#v0/composables/useDate/adapters'
57-
import type { App, ComputedRef } from 'vue'
5857
import type { ContextTrinity } from '#v0/composables/createTrinity'
58+
import type { DateAdapter } from '#v0/composables/useDate/adapters'
5959
import type { Temporal } from '@js-temporal/polyfill'
60+
import type { App, ComputedRef } from 'vue'
6061

6162
// Exports
6263
export type { DateAdapter } from '#v0/composables/useDate/adapters'
@@ -178,7 +179,7 @@ function createDateInternal<T> (
178179
let localeContext: ReturnType<typeof useLocale> | null = null
179180

180181
try {
181-
if (getCurrentInstance()) {
182+
if (instanceExists()) {
182183
localeContext = useLocale()
183184
}
184185
} catch {
@@ -193,7 +194,7 @@ function createDateInternal<T> (
193194
))
194195

195196
// Keep adapter locale in sync (only when in component scope)
196-
if (getCurrentInstance()) {
197+
if (instanceExists()) {
197198
const stop = watchEffect(() => {
198199
const loc = locale.value
199200

@@ -448,7 +449,7 @@ export function useDate<T = DefaultDateType> (namespace = 'v0:date'): DateContex
448449
// Safe cast: fallback only used when T = DefaultDateType (first overload) or context missing
449450
const fallback = createDateFallback() as unknown as DateContext<T>
450451

451-
if (!getCurrentInstance()) return fallback
452+
if (!instanceExists()) return fallback
452453

453454
try {
454455
return useContext<DateContext<T>>(namespace, fallback)

0 commit comments

Comments
 (0)