Skip to content

refactor(mosaic): read the current time through useNow instead of during render - #10098

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/mosaic-date-now-render
Oct 7, 2026
Merged

alexcarpenter merged 4 commits into
mainfrom
carp/mosaic-date-now-render

Conversation

@alexcarpenter

Copy link
Copy Markdown
Member

Description

Adds a useNow({ updateInterval }) hook to Mosaic so components stop reading the clock during render, which breaks React's purity rule. Raised in #9983 (comment).

The hook mirrors next-intl's useNow: it reads the time once in a lazy useState initializer and, when updateInterval is set, refreshes it from a setInterval in an effect.

Applied to the two existing render-time reads:

  • API keys table: lastUsedAt relative labels (now also refresh every minute).
  • Create API key dialog: the expiration date preview.

Reverification's resend countdown already uses this pattern by hand and is left as is, since it derives whether to tick from now itself.

#9983 can rebase on this and pass relativeTo: useNow({ updateInterval: 60_000 }) to formatRelative.

Checklist

  • pnpm test runs as expected. (@clerk/mosaic)
  • pnpm build runs as expected. (@clerk/mosaic)
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Oct 7, 2026 9:02pm UTC
swingset Ready Ready Preview Oct 7, 2026 9:02pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 743602d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the mosaic label Oct 6, 2026
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: ASSERTIVE
  • Plan: Team
  • Run ID: 78a54f23-7dd6-4eb7-a648-d0ba898b4ae5
📥 Commits

Reviewing files that changed from the base of the PR and between 9a17a85 and 743602d.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: ASSERTIVE
  • Plan: Team
  • Run ID: b581e392-2864-499e-997a-a2b8ecacbfc6
📥 Commits

Reviewing files that changed from the base of the PR and between 3fab8d5 and 673b8a6.

📒 Files selected for processing (3)
  • packages/mosaic/src/hooks/__tests__/use-now.test.tsx
  • packages/mosaic/src/hooks/use-now.ts
  • packages/mosaic/src/mosaic-provider.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

Included review availability: This review used your included allowance. 4 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.


📝 Walkthrough

Walkthrough

Adds the useNow hook, which returns a date from context or the current time and can update at a configured interval. MosaicProvider supplies a date captured at provider initialization. API key labels and displayed expiration dates use useNow with a 60-second interval. API key submission continues to calculate expiresAt with a fresh Date. Tests cover initial values, interval updates, provider dates, and timer cleanup.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 673b8

API key expiration is calculated when the key is submitted. The brief possibility of a stale preview near midnight does not warrant delaying the merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: using useNow instead of reading the current time during render.
Description check ✅ Passed The description explains the hook, its use in two components, and the reason for the change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@10098

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@10098

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@10098

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@10098

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@10098

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@10098

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@10098

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@10098

@clerk/expo-biometrics

npm i https://pkg.pr.new/@clerk/expo-biometrics@10098

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@10098

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@10098

@clerk/express

npm i https://pkg.pr.new/@clerk/express@10098

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@10098

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@10098

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@10098

@clerk/mosaic

npm i https://pkg.pr.new/@clerk/mosaic@10098

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@10098

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@10098

@clerk/react

npm i https://pkg.pr.new/@clerk/react@10098

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@10098

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@10098

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@10098

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@10098

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@10098

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@10098

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@10098

commit: 743602d

Comment on lines +3 to +15
export function useNow({ updateInterval }: { updateInterval?: number } = {}): Date {
const [now, setNow] = useState(() => new Date());

useEffect(() => {
if (updateInterval === undefined) {
return;
}
const id = window.setInterval(() => setNow(new Date()), updateInterval);
return () => window.clearInterval(id);
}, [updateInterval]);

return now;
}

@Ephem Ephem Oct 7, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wont work well with SSR, the state initializer will have different times on the server and the client, causing hydration mismatches.

I suggest we add the new Date() to the MosaicProvider instead and put it on a context that the useNow hook reads its initial state from. The interval still belongs here since different components might want different granularity for their reactivity (re-rendering all of these every second is heavy).

When we have that shape from the start, it's a lot easier to add SSR support by passing the new Date() result from server->client. That part would live in our framework SDKs though, they all do it a bit differently (but all already pass data server->client).

An optional improvement could be to have all the intervals at the top as well so they de-duplicate. All components that should update every minute do so together instead of on their own timer, and we end up with less timers. I think this leads to quite a bit more complexity though so I'd defer that for later/if we notice it's necessary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't been considering ssr support to much since their client components, but a good reminder to keep in mind 👍🏼 673b8a6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@Ephem Ephem left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this.

Comment on lines +3 to +15
export function useNow({ updateInterval }: { updateInterval?: number } = {}): Date {
const [now, setNow] = useState(() => new Date());

useEffect(() => {
if (updateInterval === undefined) {
return;
}
const id = window.setInterval(() => setNow(new Date()), updateInterval);
return () => window.clearInterval(id);
}, [updateInterval]);

return now;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@alexcarpenter
alexcarpenter enabled auto-merge (squash) October 7, 2026 13:50
@alexcarpenter
alexcarpenter merged commit e0cbd40 into main Oct 7, 2026
50 of 51 checks passed
@alexcarpenter
alexcarpenter deleted the carp/mosaic-date-now-render branch October 7, 2026 21:11

This branch was successfully deployed

2 active deployments
Preview – swingset — 743602d7 Deployed Oct 7, 2026 by vercel[bot]
Preview – clerk-js-sandbox — 743602d7 Deployed Oct 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants