Skip to content

Commit 02ff4f2

Browse files
authored
fix(react): Respect ui prop when Clerk instance is passed to IsomorphicClerk (#7997)
1 parent 19b6150 commit 02ff4f2

3 files changed

Lines changed: 199 additions & 2 deletions

File tree

.changeset/tricky-apples-attend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/react': minor
3+
---
4+
5+
The `ui` prop is now respected if a Clerk instance is passed via the `Clerk` prop to `IsomorphicClerk`. This fixes the 'Clerk was not loaded with Ui components' error in the Chrome Extension SDK.

packages/react/src/__tests__/isomorphicClerk.test.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,193 @@ describe('isomorphicClerk', () => {
200200
expect(result).toBe(mockClerkUI);
201201
});
202202
});
203+
204+
describe('shouldLoadUi across SDK scenarios', () => {
205+
// Helper to run getEntryChunks and return what clerk.load was called with
206+
async function runGetEntryChunks(options: Record<string, any>) {
207+
const mockLoad = vi.fn().mockResolvedValue(undefined);
208+
const mockClerkInstance = options.Clerk || {
209+
load: mockLoad,
210+
loaded: false,
211+
};
212+
if (options.Clerk) {
213+
options.Clerk.load = mockLoad;
214+
options.Clerk.loaded = false;
215+
}
216+
217+
(global as any).Clerk = mockClerkInstance;
218+
219+
const clerk = new IsomorphicClerk({
220+
publishableKey: 'pk_test_XXX',
221+
...options,
222+
});
223+
224+
await (clerk as any).getEntryChunks();
225+
226+
return { mockLoad };
227+
}
228+
229+
// ─── @clerk/react, @clerk/nextjs, @clerk/react-router, @clerk/tanstack-react-start ───
230+
// These SDKs: no Clerk prop, no ui prop, standardBrowser omitted (undefined)
231+
// shouldLoadUi = (undefined !== false && !undefined) || !!undefined = (true && true) || false = true
232+
// → loads UI from CDN
233+
it('loads UI from CDN when no Clerk, no ui, standardBrowser omitted (nextjs/react-router/tanstack)', async () => {
234+
const { mockLoad } = await runGetEntryChunks({});
235+
236+
expect(loadClerkUIScript).toHaveBeenCalled();
237+
expect(mockLoad).toHaveBeenCalledWith(
238+
expect.objectContaining({
239+
ui: expect.objectContaining({
240+
ClerkUI: (global as any).__internal_ClerkUICtor,
241+
}),
242+
}),
243+
);
244+
});
245+
246+
// ─── @clerk/react with bundled ui prop (e.g. user passes ui={ui} from @clerk/ui) ───
247+
// These SDKs: no Clerk prop, ui with ClerkUI, standardBrowser omitted
248+
// shouldLoadUi = (true && true) || true = true
249+
// → getClerkUIEntryChunk returns the bundled ClerkUI (no CDN)
250+
it('uses bundled ClerkUI when ui prop is passed without Clerk instance (react with ui prop)', async () => {
251+
const mockClerkUI = vi.fn();
252+
const { mockLoad } = await runGetEntryChunks({
253+
ui: { ClerkUI: mockClerkUI },
254+
});
255+
256+
expect(loadClerkUIScript).not.toHaveBeenCalled();
257+
expect(mockLoad).toHaveBeenCalledWith(
258+
expect.objectContaining({
259+
ui: expect.objectContaining({
260+
ClerkUI: mockClerkUI,
261+
}),
262+
}),
263+
);
264+
});
265+
266+
// ─── @clerk/expo (native mode) ───
267+
// Expo native: Clerk instance, no ui prop, standardBrowser: false
268+
// shouldLoadUi = (false !== false && ...) || !!undefined = false || false = false
269+
// → no UI loaded (correct: native apps don't render prebuilt UI)
270+
it('does not load UI for Expo native (Clerk instance, no ui, standardBrowser: false)', async () => {
271+
const mockClerkInstance = {} as any;
272+
const { mockLoad } = await runGetEntryChunks({
273+
Clerk: mockClerkInstance,
274+
standardBrowser: false,
275+
});
276+
277+
expect(loadClerkUIScript).not.toHaveBeenCalled();
278+
expect(mockLoad).toHaveBeenCalledWith(
279+
expect.objectContaining({
280+
ui: expect.objectContaining({
281+
ClerkUI: undefined,
282+
}),
283+
}),
284+
);
285+
});
286+
287+
// ─── @clerk/expo (web mode) ───
288+
// Expo web: Clerk is null, no ui prop, standardBrowser: true
289+
// shouldLoadUi = (true !== false && !null) || false = (true && true) || false = true
290+
// → loads UI from CDN (correct: web mode uses normal browser flow)
291+
it('loads UI from CDN for Expo web (Clerk: null, standardBrowser: true)', async () => {
292+
const { mockLoad } = await runGetEntryChunks({
293+
Clerk: null,
294+
standardBrowser: true,
295+
});
296+
297+
expect(loadClerkUIScript).toHaveBeenCalled();
298+
expect(mockLoad).toHaveBeenCalledWith(
299+
expect.objectContaining({
300+
ui: expect.objectContaining({
301+
ClerkUI: (global as any).__internal_ClerkUICtor,
302+
}),
303+
}),
304+
);
305+
});
306+
307+
// ─── @clerk/chrome-extension (without syncHost) ───
308+
// No syncHost: Clerk instance, ui with ClerkUI, standardBrowser: true
309+
// shouldLoadUi = (true && !instance) || true = false || true = true
310+
// → uses bundled ClerkUI (no CDN)
311+
it('uses bundled ClerkUI for chrome-extension without syncHost (standardBrowser: true)', async () => {
312+
const mockClerkUI = vi.fn();
313+
const mockClerkInstance = {} as any;
314+
const { mockLoad } = await runGetEntryChunks({
315+
Clerk: mockClerkInstance,
316+
ui: { ClerkUI: mockClerkUI },
317+
standardBrowser: true,
318+
});
319+
320+
expect(loadClerkUIScript).not.toHaveBeenCalled();
321+
expect(mockLoad).toHaveBeenCalledWith(
322+
expect.objectContaining({
323+
ui: expect.objectContaining({
324+
ClerkUI: mockClerkUI,
325+
}),
326+
}),
327+
);
328+
});
329+
330+
// ─── @clerk/chrome-extension (with syncHost) ───
331+
// With syncHost: Clerk instance, ui with ClerkUI, standardBrowser: false
332+
// shouldLoadUi = (false !== false && ...) || !!ClerkUI = false || true = true
333+
// → uses bundled ClerkUI (no CDN)
334+
it('uses bundled ClerkUI for chrome-extension with syncHost (standardBrowser: false)', async () => {
335+
const mockClerkUI = vi.fn();
336+
const mockClerkInstance = {} as any;
337+
const { mockLoad } = await runGetEntryChunks({
338+
Clerk: mockClerkInstance,
339+
ui: { ClerkUI: mockClerkUI },
340+
standardBrowser: false,
341+
});
342+
343+
expect(loadClerkUIScript).not.toHaveBeenCalled();
344+
expect(mockLoad).toHaveBeenCalledWith(
345+
expect.objectContaining({
346+
ui: expect.objectContaining({
347+
ClerkUI: mockClerkUI,
348+
}),
349+
}),
350+
);
351+
});
352+
353+
// ─── Clerk instance provided, no ui prop, standardBrowser: true ───
354+
// shouldLoadUi = (true && !instance) || false = false || false = false
355+
// → no UI loaded (correct: Clerk instance without bundled UI, no CDN attempt)
356+
it('does not load UI when Clerk instance provided without ui prop (standardBrowser: true)', async () => {
357+
const mockClerkInstance = {} as any;
358+
const { mockLoad } = await runGetEntryChunks({
359+
Clerk: mockClerkInstance,
360+
standardBrowser: true,
361+
});
362+
363+
expect(loadClerkUIScript).not.toHaveBeenCalled();
364+
expect(mockLoad).toHaveBeenCalledWith(
365+
expect.objectContaining({
366+
ui: expect.objectContaining({
367+
ClerkUI: undefined,
368+
}),
369+
}),
370+
);
371+
});
372+
373+
// ─── ui prop passed as server marker (no ClerkUI), no Clerk instance ───
374+
// RSC react-server export may provide ui without ClerkUI initially
375+
// shouldLoadUi = (true && true) || false = true
376+
// → getClerkUIEntryChunk is called, but uiProp exists without ClerkUI → returns undefined (skips CDN)
377+
it('skips CDN when ui prop exists without ClerkUI (server marker object)', async () => {
378+
const { mockLoad } = await runGetEntryChunks({
379+
ui: { __brand: '__clerkUI', version: '1.0.0' },
380+
});
381+
382+
expect(loadClerkUIScript).not.toHaveBeenCalled();
383+
expect(mockLoad).toHaveBeenCalledWith(
384+
expect.objectContaining({
385+
ui: expect.objectContaining({
386+
ClerkUI: undefined,
387+
}),
388+
}),
389+
);
390+
});
391+
});
203392
});

packages/react/src/isomorphicClerk.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
514514

515515
if (!clerk.loaded) {
516516
this.beforeLoad(clerk);
517-
// Only load UI scripts in standard browser environments (not native/headless)
518-
const shouldLoadUi = !this.options.Clerk && this.options.standardBrowser !== false;
517+
// Load UI when:
518+
// - standard browser and no pre-created Clerk instance (normal CDN path), OR
519+
// - a bundled ClerkUI was provided via the ui prop (e.g. chrome-extension, even with standardBrowser: false)
520+
const shouldLoadUi =
521+
(this.options.standardBrowser !== false && !this.options.Clerk) || !!this.options.ui?.ClerkUI;
519522
const ClerkUI = shouldLoadUi ? await this.getClerkUIEntryChunk() : undefined;
520523
await clerk.load({ ...this.options, ui: { ...this.options.ui, ClerkUI } });
521524
}

0 commit comments

Comments
 (0)