Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-times-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

RelativeTime: Prevent hydration errors when the server and client use different time zones
41 changes: 40 additions & 1 deletion packages/react/src/RelativeTime/RelativeTime.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {describe, expect, it} from 'vitest'
import {act} from 'react'
import {hydrateRoot, type Root} from 'react-dom/client'
import {renderToString} from 'react-dom/server'
import {describe, expect, it, vi} from 'vitest'
import RelativeTime from '.'
import {render} from '@testing-library/react'
import {implementsClassName} from '../utils/testing'
Expand Down Expand Up @@ -35,6 +38,42 @@ describe('RelativeTime', () => {
expect(container.textContent).toEqual('server rendered date')
})

it('hydrates the fallback without errors when server and client time zones differ', async () => {
const date = new Date('2024-03-07T00:30:00.000Z')
const relativeTime = <RelativeTime date={date} />
const toLocaleDateStringSpy = vi.spyOn(Date.prototype, 'toLocaleDateString').mockReturnValue('Mar 7, 2024')
const container = document.createElement('div')
container.innerHTML = renderToString(relativeTime)
document.body.appendChild(container)

toLocaleDateStringSpy.mockImplementation((_locales, options) =>
options?.timeZone === 'UTC' ? 'Mar 7, 2024' : 'Mar 6, 2024',
)
Comment on lines +44 to +51

const recoverableErrors: unknown[] = []
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
let root: Root | undefined

try {
expect(container.firstChild).toHaveTextContent('Mar 7, 2024')

await act(async () => {
root = hydrateRoot(container, relativeTime, {
onRecoverableError: error => recoverableErrors.push(error),
})
})

expect(recoverableErrors).toEqual([])
expect(consoleErrorSpy).not.toHaveBeenCalled()
expect(container.firstChild).toHaveTextContent('Mar 7, 2024')
} finally {
toLocaleDateStringSpy.mockRestore()
consoleErrorSpy.mockRestore()
await act(async () => root?.unmount())
container.remove()
}
})

it('does not render no-title attribute by default', () => {
const date = new Date('2024-03-07T12:22:48.123Z')
const {container} = render(<RelativeTime date={date} />)
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/RelativeTime/RelativeTime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import {createComponent} from '../utils/create-component'

const RelativeTimeComponent = createComponent(RelativeTimeElement, 'relative-time')

const localeOptions: Intl.DateTimeFormatOptions = {month: 'short', day: 'numeric', year: 'numeric'}
const localeOptions: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
} satisfies Intl.DateTimeFormatOptions
Comment on lines +7 to +12
function RelativeTime({date, datetime, children, noTitle, ...props}: RelativeTimeProps) {
if (datetime) date = new Date(datetime)
return (
Expand Down
Loading