Skip to content

fix(react): attach UI for preloaded Clerk instances#8594

Open
jacekradko wants to merge 5 commits into
mainfrom
jacek/fix-preloaded-clerk-ui-attachment
Open

fix(react): attach UI for preloaded Clerk instances#8594
jacekradko wants to merge 5 commits into
mainfrom
jacek/fix-preloaded-clerk-ui-attachment

Conversation

@jacekradko
Copy link
Copy Markdown
Member

@jacekradko jacekradko commented May 19, 2026

Fixes #8569.

When React's IsomorphicClerk reused an already-loaded global Clerk instance, it skipped the UI chunk load and immediately replayed premounted UI invocations, so sign-in/sign-up etc. never rendered. Now we load the UI chunk and call clerk.load again on the existing instance, which short-circuits the resource fetch but attaches ClerkUI via the new #initClerkUI path.

Includes regression tests for both packages and a patch changeset.

Before:

screenshot-broken

After:

screenshot-fixed

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

🦋 Changeset detected

Latest commit: 6e983d7

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

This PR includes changesets to release 7 packages
Name Type
@clerk/clerk-js Patch
@clerk/react Patch
@clerk/chrome-extension Patch
@clerk/expo Patch
@clerk/nextjs Patch
@clerk/react-router Patch
@clerk/tanstack-react-start Patch

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

@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 3, 2026 3:07am

Request Review

@jacekradko jacekradko marked this pull request as ready for review May 19, 2026 14:01
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors Clerk.load() to initialize ClerkUI via a guarded #initClerkUI(), adds __internal_attachClerkUI to attach a ClerkUI constructor (or promise) to an already-loaded Clerk, updates React's isomorphic loader to fetch and attach UI for preloaded Clerk instances, adds a shared type for the internal hook, and includes tests plus a changeset.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing UI attachment for preloaded Clerk instances in React, which directly aligns with the PR's primary objective.
Description check ✅ Passed The description clearly explains the bug fix, including the problem (UI not loading for preloaded instances), the solution (loading UI chunk and calling clerk.load again), and includes before/after screenshots demonstrating the fix.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #8569: it enables UI attachment for already-loaded Clerk instances by implementing __internal_attachClerkUI, ensures clerk.load() handles preloaded scenarios, and prevents duplicate network requests through short-circuiting.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fix the preloaded Clerk UI attachment issue: type definitions, core implementation in clerk.ts, isomorphic clerk handling, and comprehensive regression tests. No unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 19, 2026

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: 6e983d7

Comment thread packages/clerk-js/src/core/clerk.ts Outdated
Comment on lines +524 to +525
const nextOptions = this.#initOptions({ ...this.#options, ...options });
this.#options = nextOptions;
Copy link
Copy Markdown
Member

@Ephem Ephem May 20, 2026

Choose a reason for hiding this comment

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

This part worries me a bit. After the initial load, the options have always been immutable before, now they can theoretically change, but they aren't reactive so if we've read them anywhere already, those values might now be stale (imagine clerk-js loads -> components in the host app rely on some options via the hooks -> ui loads with other options).

Supporting options updating for real with reactivity would be a huge pain for no real gain.

I think a clean alternative is to lift this to an __internal_attachUi(ui) function that's only callable after the initial load?

Internal only to avoid increasing the API surface needlessly for an advanced feature only we are likely to use, could make it public if there is demand.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/shared/src/types/clerk.ts (1)

248-254: ⚡ Quick win

Document the new exported hook more fully.

Since this sits on the exported Clerk interface, please add a short summary plus @param / @returns details instead of only @internal. That keeps the generated/public type surface self-explanatory even for internal hooks.

As per coding guidelines, "All public APIs must be documented with JSDoc".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/clerk.ts` around lines 248 - 254, Add full JSDoc
for the exported __internal_attachClerkUI property on the Clerk interface:
replace the lone `@internal` tag with a short one-line summary describing its
purpose, add `@param` entries for ClerkUI (ClerkUIConstructor |
Promise<ClerkUIConstructor>) and options (ClerkOptions | undefined) explaining
what each is and when to pass them, and add an `@returns` describing the return
type (void). Keep the `@internal` tag if needed but ensure the summary and
`@param/`@returns are present so the public type surface is self-explanatory;
reference the __internal_attachClerkUI property, ClerkUIConstructor, and
ClerkOptions when writing the docs.
packages/react/src/__tests__/isomorphicClerk.test.ts (1)

246-279: ⚡ Quick win

Add a regression test for the preloaded-instance fallback path.

This only exercises the __internal_attachClerkUI branch. getEntryChunks() still falls back to clerk.load(options) when a preloaded Clerk instance does not expose that hook, so a sibling test for that older-instance path would better protect the compatibility contract here.

As per coding guidelines, "Non-major releases in packages/clerk-js and packages/ui must remain backwards-compatible with SDK versions already in the wild. Changes must not remove or rename anything that older SDK versions still call, as this will break production for those users."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/__tests__/isomorphicClerk.test.ts` around lines 246 - 279,
Add a sibling test that simulates a preloaded global Clerk instance which does
NOT expose __internal_attachClerkUI so the IsomorphicClerk.getEntryChunks()
fallback to calling clerk.load(options) is exercised; specifically, create a
mockClerkInstance with loaded: true and a mock load method (but no
__internal_attachClerkUI), assign it to global.Clerk, instantiate
IsomorphicClerk, call mountUserButton and then await getEntryChunks(), and
assert that loadClerkUIScript was called and that mock load was invoked (and
__internal_attachClerkUI was not), ensuring the flow through
IsomorphicClerk.getEntryChunks -> clerk.load is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/react/src/__tests__/isomorphicClerk.test.ts`:
- Around line 246-279: Add a sibling test that simulates a preloaded global
Clerk instance which does NOT expose __internal_attachClerkUI so the
IsomorphicClerk.getEntryChunks() fallback to calling clerk.load(options) is
exercised; specifically, create a mockClerkInstance with loaded: true and a mock
load method (but no __internal_attachClerkUI), assign it to global.Clerk,
instantiate IsomorphicClerk, call mountUserButton and then await
getEntryChunks(), and assert that loadClerkUIScript was called and that mock
load was invoked (and __internal_attachClerkUI was not), ensuring the flow
through IsomorphicClerk.getEntryChunks -> clerk.load is covered.

In `@packages/shared/src/types/clerk.ts`:
- Around line 248-254: Add full JSDoc for the exported __internal_attachClerkUI
property on the Clerk interface: replace the lone `@internal` tag with a short
one-line summary describing its purpose, add `@param` entries for ClerkUI
(ClerkUIConstructor | Promise<ClerkUIConstructor>) and options (ClerkOptions |
undefined) explaining what each is and when to pass them, and add an `@returns`
describing the return type (void). Keep the `@internal` tag if needed but ensure
the summary and `@param/`@returns are present so the public type surface is
self-explanatory; reference the __internal_attachClerkUI property,
ClerkUIConstructor, and ClerkOptions when writing the docs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: e4fec411-6322-47aa-8d35-362959663505

📥 Commits

Reviewing files that changed from the base of the PR and between d42d100 and edd80a3.

📒 Files selected for processing (5)
  • packages/clerk-js/src/core/__tests__/clerk.test.ts
  • packages/clerk-js/src/core/clerk.ts
  • packages/react/src/__tests__/isomorphicClerk.test.ts
  • packages/react/src/isomorphicClerk.ts
  • packages/shared/src/types/clerk.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/react/src/isomorphicClerk.ts
  • packages/clerk-js/src/core/clerk.ts

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

API Changes Report

Generated by Break Check on 2026-06-03T03:10:48.295Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 3
🔴 Breaking changes 0
🟡 Non-breaking changes 4
🟢 Additions 2

🤖 This report was reviewed by claude-sonnet-4-6.

Note
Break Check could not snapshot 3 subpaths; the diff below excludes them.

  • @clerk/astro ./env: Internal Error: Unable to determine module for: /home/runner/_work/javascript/javascript/packages/astro/env.d.ts You have encountered a software defect. Please consider reporting the issue to the maintainers of this application.
  • @clerk/shared ./cookie: Internal Error: Unable to follow symbol for "Cookies" You have encountered a software defect. Please consider reporting the issue to the maintainers of this application.
  • @clerk/testing ./cypress: Symbol not found for identifier: Cypress

@clerk/astro

Current version: 3.3.2
Recommended bump: MINOR → 3.4.0

Subpath ./client

🟡 Non-breaking Changes (3)

Modified: $authStore
  $authStore: nanostores.ReadableAtom<{
      userId: string | null | undefined;
-     user: _clerk_shared__chunks_index_dZ_J_o0Z.ps | null | undefined;
+     user: _clerk_shared__chunks_index_BTO2FAtE.ps | null | undefined;
      sessionId: string | null | undefined;
-     session: _clerk_shared__chunks_index_dZ_J_o0Z.Qo | null | undefined;
+     session: _clerk_shared__chunks_index_BTO2FAtE.Qo | null | undefined;
      sessionStatus: "active" | "pending" | undefined;
-     sessionClaims: _clerk_shared__chunks_index_dZ_J_o0Z.to | null | undefined;
-     organization: _clerk_shared__chunks_index_dZ_J_o0Z.xo | null | undefined;
+     sessionClaims: _clerk_shared__chunks_index_BTO2FAtE.to | null | undefined;
+     organization: _clerk_shared__chunks_index_BTO2FAtE.xo | null | undefined;
      orgId: string | null | undefined;
      orgRole: string | null | undefined;
      orgSlug: string | null | undefined;
-     orgPermissions: _clerk_shared__chunks_index_dZ_J_o0Z.gm<_clerk_shared__chunks_index_dZ_J_o0Z.co>[] | null | undefined;
-     actor: _clerk_shared__chunks_index_dZ_J_o0Z.Xa | null | undefined;
+     orgPermissions: _clerk_shared__chunks_index_BTO2FAtE.gm<_clerk_shared__chunks_index_BTO2FAtE.co>[] | null | undefined;
+     actor: _clerk_shared__chunks_index_BTO2FAtE.Xa | null | undefined;
      factorVerificationAge: [number, number] | null;
  }>

Static analyzer: Breaking change in variable $authStore: Type changed: $authStore:import("nanostores").ReadableAtom<{userId:string|null|undefined;user:!_clerk_shared__chunks_index_dZ_J_o0Z.p…$authStore:import("nanostores").ReadableAtom<{userId:string|null|undefined;user:!_clerk_shared__chunks_index_BTO2FAtE.p…

🤖 AI review (reclassified as non-breaking) (90%): Only the internal chunk filename hash changed (dZ_J_o0ZBTO2FAtE); all exported field names and their structural types (ps, Qo, to, xo, gm, co, Xa) remain identical — this is a build artifact rename with no observable shape change for consumers.

Modified: $organizationStore
- $organizationStore: nanostores.ReadableAtom<_clerk_shared__chunks_index_dZ_J_o0Z.xo | null | undefined>
+ $organizationStore: nanostores.ReadableAtom<_clerk_shared__chunks_index_BTO2FAtE.xo | null | undefined>

Static analyzer: Breaking change in variable $organizationStore: Type changed: $organizationStore:import("nanostores").ReadableAtom<!_clerk_shared__chunks_index_dZ_J_o0Z.xo:type|null|undefined>$organizationStore:import("nanostores").ReadableAtom<!_clerk_shared__chunks_index_BTO2FAtE.xo:type|null|undefined>

🤖 AI review (reclassified as non-breaking) (90%): The type alias xo is unchanged structurally; only the internal chunk module filename hash differs, so consumers reading $organizationStore see the same resolved type.

Modified: $userStore
- $userStore: nanostores.ReadableAtom<_clerk_shared__chunks_index_dZ_J_o0Z.ps | null | undefined>
+ $userStore: nanostores.ReadableAtom<_clerk_shared__chunks_index_BTO2FAtE.ps | null | undefined>

Static analyzer: Breaking change in variable $userStore: Type changed: $userStore:import("nanostores").ReadableAtom<!_clerk_shared__chunks_index_dZ_J_o0Z.ps:type|null|undefined>$userStore:import("nanostores").ReadableAtom<!_clerk_shared__chunks_index_BTO2FAtE.ps:type|null|undefined>

🤖 AI review (reclassified as non-breaking) (90%): The type alias ps is unchanged structurally; only the internal chunk module filename hash differs, so consumers reading $userStore see the same resolved type.


@clerk/clerk-js

Current version: 6.13.0
Recommended bump: MINOR → 6.14.0

Subpath .

🟢 Additions (1)

Added: Clerk.__internal_attachClerkUI
+ __internal_attachClerkUI: (ClerkUI: ClerkUIConstructor | Promise<ClerkUIConstructor>, options?: ClerkOptions) => void;

Added property Clerk.__internal_attachClerkUI

Subpath ./no-rhc

🟢 Additions (1)

Added: Clerk.__internal_attachClerkUI
+ __internal_attachClerkUI: (ClerkUI: ClerkUIConstructor | Promise<ClerkUIConstructor>, options?: ClerkOptions) => void;

Added property Clerk.__internal_attachClerkUI


@clerk/shared

Current version: 4.14.0
Recommended bump: MINOR → 4.15.0

Subpath ./apiUrlFromPublishableKey

🟡 Non-breaking Changes (1)

Modified: apiUrlFromPublishableKey
- apiUrlFromPublishableKey: (publishableKey: string) => "https://api.lclclerk.com" | "https://api.clerkstage.dev" | "https://api.clerk.com"
+ apiUrlFromPublishableKey: (publishableKey: string) => "https://api.clerk.com" | "https://api.lclclerk.com" | "https://api.clerkstage.dev"

Static analyzer: Breaking change in function apiUrlFromPublishableKey: Return type changed: "https://api.lclclerk.com"|"https://api.clerkstage.dev"|"https://api.clerk.com""https://api.clerk.com"|"https://api.lclclerk.com"|"https://api.clerkstage.dev"

🤖 AI review (reclassified as non-breaking) (99%): The union members are identical; only their order changed. TypeScript union types are structurally equivalent regardless of member ordering, so no well-typed consumer code is affected.


Report generated by Break Check

Last ran on 6e983d7. Pushes that change no tracked declarations (no API surface change vs. base) are skipped and don't update this comment.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/shared/src/types/clerk.ts (1)

1655-1655: ⚡ Quick win

Keep internal caller unions aligned for type safety.

__internal_AttemptToEnableEnvironmentSettingParams['caller'] now includes 'ConfigureSSO', but __internal_EnableOrganizationsPromptProps['caller'] still excludes it, so core currently needs a cast when forwarding this value. Add 'ConfigureSSO' there as well to keep these internal contracts in sync.

Proposed diff
 export type __internal_EnableOrganizationsPromptProps = {
   onSuccess?: () => void;
   onClose?: () => void;
 } & {
   caller:
     | 'OrganizationSwitcher'
     | 'OrganizationProfile'
     | 'OrganizationList'
+    | 'ConfigureSSO'
     | 'useOrganizationList'
     | 'useOrganization';
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/clerk.ts` at line 1655, The union type for the
internal caller enums is out of sync:
__internal_AttemptToEnableEnvironmentSettingParams['caller'] now includes
'ConfigureSSO' but __internal_EnableOrganizationsPromptProps['caller'] does not,
forcing casts when forwarding values. Update the declaration of
__internal_EnableOrganizationsPromptProps['caller'] to include the
'ConfigureSSO' member (mirror the same union used by
__internal_AttemptToEnableEnvironmentSettingParams) so both internal caller
unions remain aligned and remove the need for casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/shared/src/types/clerk.ts`:
- Line 1655: The union type for the internal caller enums is out of sync:
__internal_AttemptToEnableEnvironmentSettingParams['caller'] now includes
'ConfigureSSO' but __internal_EnableOrganizationsPromptProps['caller'] does not,
forcing casts when forwarding values. Update the declaration of
__internal_EnableOrganizationsPromptProps['caller'] to include the
'ConfigureSSO' member (mirror the same union used by
__internal_AttemptToEnableEnvironmentSettingParams) so both internal caller
unions remain aligned and remove the need for casts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 5889aa1d-7d94-441b-a667-5cb09f88d630

📥 Commits

Reviewing files that changed from the base of the PR and between edd80a3 and 6e983d7.

📒 Files selected for processing (2)
  • packages/clerk-js/src/core/clerk.ts
  • packages/shared/src/types/clerk.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

loadClerkJsScript broken clerk/react

2 participants