Skip to content

fix(ui): render relative times stably through hydration - #5058

Merged
swati510 merged 4 commits into
mainfrom
fix/ui-hydration-stable-time
Oct 31, 2026
Merged

swati510 merged 4 commits into
mainfrom
fix/ui-hydration-stable-time

Conversation

@RaghavChamadiya

Copy link
Copy Markdown
Member

What

Relative times ("3d ago", "Since the previous index, 2d ago") rendered on the server could differ from the text the browser computes during hydration. That raised a React text-mismatch error, and React threw away the server HTML.

How

  • New RelativeTime component in packages/ui/src/shared. On the server and during hydration it renders a fixed UTC date. After hydration it renders the age from the viewer's clock, inside a <time dateTime title> element.
  • Hosts pass plain strings: template (with {time}), words (per-unit wording with {value}) and fallback. Server components can therefore localise the text without freezing the age at render time.
  • formatRelativeTime accepts the same optional words map. English output is unchanged.
  • Converted call sites: overview (change line, hotspots, activity lists, repo rows), docs reader, owners, dead code, security, coverage, files, wiki history, chat history, dashboard widgets, and the web jobs and changes tables.
  • ChangeLineCopy.relativeTime is replaced by ago (per-unit words); the web overview copy builds it from its i18n messages.
  • The health page header drops its private formatIndexedAt and uses the shared formatter.

Verification

  • ui and web tsc --noEmit: clean.
  • ui vitest: 3050 tests passing.
  • web vitest (jobs, changes, overview): passing.
  • New hydration tests render on the server, advance the clock, then hydrate. They check there is no recoverable error and that the hydrated text follows the client clock, including when the template was built at server time.

A page rendered and cached on the server is hydrated later, under the
viewer's clock. A relative time ("3d ago") computed during render can then
differ from the server HTML across a unit boundary, which throws React
error 418 and discards the server render.

Add RelativeTime: it renders the UTC date on the server and during
hydration, then the relative text. Host-worded text passes through as
children, with an optional stable twin. ChangeLine, the overview hotspot
and activity rows, and the other render-time relative times use it.
RelativeTime took host-computed text as children. Under a server
component that text is computed once on the server, so after hydration
the page showed an age frozen at render or cache time.

RelativeTime now always measures the age itself, from the viewer's clock.
Hosts word it with plain strings that cross the client boundary: a
template with a {time} placeholder, per-bucket words with a {value}
placeholder (formatRelativeTime takes the same map), and a fallback for a
missing or future date. ChangeLineCopy.relativeTime becomes ago words, and
packages/web builds them from overview.ago.*. The time renders in a
<time> element titled with the full UTC date-time.
@swati510
swati510 merged commit a07a537 into main Oct 31, 2026
14 checks passed
@swati510
swati510 deleted the fix/ui-hydration-stable-time branch October 31, 2026 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants