From 5e53e7fb83da0216ed1a44f6bb9e099308b3b66e Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 26 Aug 2025 21:32:51 +0300 Subject: [PATCH 1/8] fix(clerk-js,clerk-react): Avoid CLS when fallback is passed to `` --- .changeset/floppy-glasses-share.md | 6 +++ .../components/PricingTable/PricingTable.tsx | 22 ++++++++++ .../src/ui/elements/InvisibleRootBox.tsx | 7 +++- .../src/ui/elements/contexts/index.tsx | 13 +++++- .../src/utils/useWaitForComponentMount.ts | 42 +++++++++++++------ 5 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 .changeset/floppy-glasses-share.md diff --git a/.changeset/floppy-glasses-share.md b/.changeset/floppy-glasses-share.md new file mode 100644 index 00000000000..e4989d572f1 --- /dev/null +++ b/.changeset/floppy-glasses-share.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/clerk-react': patch +--- + +Wait for pricing table data to be ready before hiding its fallback. diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx index c6eee496a45..9e85e66dd3d 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx @@ -3,11 +3,32 @@ import type { CommercePlanResource, CommerceSubscriptionPlanPeriod, PricingTable import { useEffect, useMemo, useState } from 'react'; import { Flow } from '@/ui/customizables/Flow'; +import { useFlowMetadata } from '@/ui/elements/contexts'; import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscription } from '../../contexts'; import { PricingTableDefault } from './PricingTableDefault'; import { PricingTableMatrix } from './PricingTableMatrix'; +function SyncRootElement() { + const clerk = useClerk(); + const { data: subscription, isLoading: isSubscriptionLoading } = useSubscription(); + const { data: plans, isLoading: isPlansLoading } = usePlans(); + const { rootElement } = useFlowMetadata(); + + useEffect(() => { + if (isSubscriptionLoading || isPlansLoading) { + return; + } + const isReady = clerk.isSignedIn ? !!subscription : plans.length > 0; + + if (isReady) { + rootElement?.setAttribute('data-ready', 'true'); + } + }, [subscription, plans]); + + return null; +} + const PricingTableRoot = (props: PricingTableProps) => { const clerk = useClerk(); const { mode = 'mounted', signInMode = 'redirect' } = usePricingTableContext(); @@ -78,6 +99,7 @@ const PricingTableRoot = (props: PricingTableProps) => { width: '100%', }} > + {mode !== 'modal' && (props as any).layout === 'matrix' ? ( ; const _InvisibleRootBox = React.memo((props: RootBoxProps) => { const [showSpan, setShowSpan] = React.useState(true); + const { setRootElement } = useFlowMetadata(); const parentRef = React.useRef(null); React.useEffect(() => { @@ -24,7 +26,10 @@ const _InvisibleRootBox = React.memo((props: RootBoxProps) => { {props.children} {showSpan && ( (parentRef.current = el ? el.parentElement : parentRef.current)} + ref={el => { + parentRef.current = el ? el.parentElement : parentRef.current; + setRootElement(parentRef.current); + }} aria-hidden style={{ display: 'none' }} /> diff --git a/packages/clerk-js/src/ui/elements/contexts/index.tsx b/packages/clerk-js/src/ui/elements/contexts/index.tsx index 84aa3211f39..789b19e5326 100644 --- a/packages/clerk-js/src/ui/elements/contexts/index.tsx +++ b/packages/clerk-js/src/ui/elements/contexts/index.tsx @@ -125,11 +125,20 @@ export type FlowMetadata = { | 'accountSwitcher'; }; -const [FlowMetadataCtx, useFlowMetadata] = createContextAndHook('FlowMetadata'); +const [FlowMetadataCtx, useFlowMetadata] = createContextAndHook< + FlowMetadata & { + rootElement: HTMLElement | null; + setRootElement: React.Dispatch>; + } +>('FlowMetadata'); export const FlowMetadataProvider = (props: React.PropsWithChildren) => { const { flow, part } = props; - const value = React.useMemo(() => ({ value: props }), [flow, part]); + const [rootElement, setRootElement] = React.useState(null); + const value = React.useMemo( + () => ({ value: { ...props, rootElement, setRootElement } }), + [flow, part, rootElement, setRootElement], + ); return {props.children}; }; diff --git a/packages/react/src/utils/useWaitForComponentMount.ts b/packages/react/src/utils/useWaitForComponentMount.ts index dc6019b65fb..6edcc80f271 100644 --- a/packages/react/src/utils/useWaitForComponentMount.ts +++ b/packages/react/src/utils/useWaitForComponentMount.ts @@ -1,10 +1,15 @@ import { useEffect, useRef, useState } from 'react'; /** - * Used to detect when a Clerk component has been added to the DOM. + * Used to detect when a Clerk component has been added to the DOM or meets a custom readiness check. */ -function waitForElementChildren(options: { selector?: string; root?: HTMLElement | null; timeout?: number }) { - const { root = document?.body, selector, timeout = 0 } = options; +function waitForElementChildren(options: { + selector?: string; + root?: HTMLElement | null; + timeout?: number; + check?: (el: HTMLElement | null) => boolean; +}) { + const { root = document?.body, selector, timeout = 0, check } = options; return new Promise((resolve, reject) => { if (!root) { @@ -17,9 +22,15 @@ function waitForElementChildren(options: { selector?: string; root?: HTMLElement elementToWatch = root?.querySelector(selector); } - // Check if the element already has child nodes - const isElementAlreadyPresent = elementToWatch?.childElementCount && elementToWatch.childElementCount > 0; - if (isElementAlreadyPresent) { + const isReady = (el: HTMLElement | null) => { + if (typeof check === 'function') { + return !!check(el); + } + return !!(el?.childElementCount && el.childElementCount > 0); + }; + + // Initial readiness check + if (isReady(elementToWatch)) { resolve(); return; } @@ -27,12 +38,12 @@ function waitForElementChildren(options: { selector?: string; root?: HTMLElement // Set up a MutationObserver to detect when the element has children const observer = new MutationObserver(mutationsList => { for (const mutation of mutationsList) { - if (mutation.type === 'childList') { - if (!elementToWatch && selector) { - elementToWatch = root?.querySelector(selector); - } + if (!elementToWatch && selector) { + elementToWatch = root?.querySelector(selector); + } - if (elementToWatch?.childElementCount && elementToWatch.childElementCount > 0) { + if (mutation.type === 'childList' || mutation.type === 'attributes') { + if (isReady(elementToWatch)) { observer.disconnect(); resolve(); return; @@ -41,7 +52,7 @@ function waitForElementChildren(options: { selector?: string; root?: HTMLElement } }); - observer.observe(root, { childList: true, subtree: true }); + observer.observe(root, { childList: true, attributes: true, subtree: true }); // Set up an optional timeout to reject the promise if the element never gets child nodes if (timeout > 0) { @@ -66,7 +77,12 @@ export function useWaitForComponentMount(component?: string) { } if (typeof window !== 'undefined' && !watcherRef.current) { - watcherRef.current = waitForElementChildren({ selector: `[data-clerk-component="${component}"]` }) + const selector = `[data-clerk-component="${component}"]`; + const needsReadyAttribute = component === 'PricingTable'; + watcherRef.current = waitForElementChildren({ + selector, + check: needsReadyAttribute ? el => el?.getAttribute('data-ready') === 'true' : undefined, + }) .then(() => { setStatus('rendered'); }) From ab7d50329e9c1efd439ee928d2cb8ab615858c75 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 26 Aug 2025 22:05:26 +0300 Subject: [PATCH 2/8] improve --- .../react/src/components/uiComponents.tsx | 2 +- .../src/utils/useWaitForComponentMount.ts | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index 18623690ad1..41462c25bf6 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -129,7 +129,7 @@ const CustomPortalsRenderer = (props: CustomPortalsRendererProps) => { export const SignIn = withClerk( ({ clerk, component, fallback, ...props }: WithClerkProp) => { - const mountingStatus = useWaitForComponentMount(component); + const mountingStatus = useWaitForComponentMount(component, { selector: '[data-ready="true"]' }); const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; const rendererRootProps = { diff --git a/packages/react/src/utils/useWaitForComponentMount.ts b/packages/react/src/utils/useWaitForComponentMount.ts index 6edcc80f271..0928a8036c9 100644 --- a/packages/react/src/utils/useWaitForComponentMount.ts +++ b/packages/react/src/utils/useWaitForComponentMount.ts @@ -6,10 +6,10 @@ import { useEffect, useRef, useState } from 'react'; function waitForElementChildren(options: { selector?: string; root?: HTMLElement | null; + readySelector?: string; timeout?: number; - check?: (el: HTMLElement | null) => boolean; }) { - const { root = document?.body, selector, timeout = 0, check } = options; + const { root = document?.body, selector, readySelector, timeout = 0 } = options; return new Promise((resolve, reject) => { if (!root) { @@ -23,8 +23,11 @@ function waitForElementChildren(options: { } const isReady = (el: HTMLElement | null) => { - if (typeof check === 'function') { - return !!check(el); + if (readySelector) { + if (el?.matches?.(readySelector)) { + return true; + } + return !!el?.querySelector?.(readySelector); } return !!(el?.childElementCount && el.childElementCount > 0); }; @@ -67,7 +70,10 @@ function waitForElementChildren(options: { /** * Detect when a Clerk component has mounted by watching DOM updates to an element with a `data-clerk-component="${component}"` property. */ -export function useWaitForComponentMount(component?: string) { +export function useWaitForComponentMount( + component?: string, + options?: { selector: string }, +): 'rendering' | 'rendered' | 'error' { const watcherRef = useRef>(); const [status, setStatus] = useState<'rendering' | 'rendered' | 'error'>('rendering'); @@ -78,11 +84,8 @@ export function useWaitForComponentMount(component?: string) { if (typeof window !== 'undefined' && !watcherRef.current) { const selector = `[data-clerk-component="${component}"]`; - const needsReadyAttribute = component === 'PricingTable'; - watcherRef.current = waitForElementChildren({ - selector, - check: needsReadyAttribute ? el => el?.getAttribute('data-ready') === 'true' : undefined, - }) + const readySelector = options?.selector; + watcherRef.current = waitForElementChildren({ selector, readySelector }) .then(() => { setStatus('rendered'); }) @@ -90,7 +93,7 @@ export function useWaitForComponentMount(component?: string) { setStatus('error'); }); } - }, [component]); + }, [component, options?.selector]); return status; } From 051c3cad324160d05d186f362316e20c5fd72acf Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 26 Aug 2025 22:43:14 +0300 Subject: [PATCH 3/8] wip --- .../react/src/components/uiComponents.tsx | 5 +- .../src/utils/useWaitForComponentMount.ts | 129 ++++++++++-------- 2 files changed, 77 insertions(+), 57 deletions(-) diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index 41462c25bf6..55ff15bca7f 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -129,7 +129,7 @@ const CustomPortalsRenderer = (props: CustomPortalsRendererProps) => { export const SignIn = withClerk( ({ clerk, component, fallback, ...props }: WithClerkProp) => { - const mountingStatus = useWaitForComponentMount(component, { selector: '[data-ready="true"]' }); + const mountingStatus = useWaitForComponentMount(component); const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; const rendererRootProps = { @@ -577,8 +577,9 @@ export const Waitlist = withClerk( export const PricingTable = withClerk( ({ clerk, component, fallback, ...props }: WithClerkProp) => { - const mountingStatus = useWaitForComponentMount(component); + const mountingStatus = useWaitForComponentMount(component, { selector: '[data-ready="true"]' }); const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; + console.log('shouldShowFallback', shouldShowFallback, mountingStatus); const rendererRootProps = { ...(shouldShowFallback && fallback && { style: { display: 'none' } }), diff --git a/packages/react/src/utils/useWaitForComponentMount.ts b/packages/react/src/utils/useWaitForComponentMount.ts index 0928a8036c9..be319bfb147 100644 --- a/packages/react/src/utils/useWaitForComponentMount.ts +++ b/packages/react/src/utils/useWaitForComponentMount.ts @@ -1,71 +1,83 @@ import { useEffect, useRef, useState } from 'react'; -/** - * Used to detect when a Clerk component has been added to the DOM or meets a custom readiness check. - */ -function waitForElementChildren(options: { - selector?: string; - root?: HTMLElement | null; - readySelector?: string; - timeout?: number; -}) { - const { root = document?.body, selector, readySelector, timeout = 0 } = options; +const createAwaitableMutationObserver = ( + globalOptions: MutationObserverInit & { + isReady: (el: HTMLElement | null, selector: string) => boolean; + }, +) => { + const isReady = globalOptions?.isReady; - return new Promise((resolve, reject) => { - if (!root) { - reject(new Error('No root element provided')); - return; - } + return (options: { selector: string; root?: HTMLElement | null; timeout?: number }) => + new Promise((resolve, reject) => { + console.log('Selector', options.selector); + const { root = document?.body, selector, timeout = 0 } = options; - let elementToWatch: HTMLElement | null = root; - if (selector) { - elementToWatch = root?.querySelector(selector); - } + if (!root) { + reject(new Error('No root element provided')); + return; + } - const isReady = (el: HTMLElement | null) => { - if (readySelector) { - if (el?.matches?.(readySelector)) { - return true; - } - return !!el?.querySelector?.(readySelector); + let elementToWatch: HTMLElement | null = root; + if (selector) { + elementToWatch = root?.querySelector(selector); } - return !!(el?.childElementCount && el.childElementCount > 0); - }; - // Initial readiness check - if (isReady(elementToWatch)) { - resolve(); - return; - } + // Initial readiness check + if (isReady(elementToWatch, selector)) { + resolve(); + return; + } - // Set up a MutationObserver to detect when the element has children - const observer = new MutationObserver(mutationsList => { - for (const mutation of mutationsList) { - if (!elementToWatch && selector) { - elementToWatch = root?.querySelector(selector); - } + // Set up a MutationObserver to detect when the element has children + const observer = new MutationObserver(mutationsList => { + for (const mutation of mutationsList) { + console.log('Mutation', mutation); + if (!elementToWatch && selector) { + elementToWatch = root?.querySelector(selector); + } + console.log('elementToWatch', elementToWatch); - if (mutation.type === 'childList' || mutation.type === 'attributes') { - if (isReady(elementToWatch)) { - observer.disconnect(); - resolve(); - return; + if ( + (globalOptions.childList && mutation.type === 'childList') || + (globalOptions.attributes && mutation.type === 'attributes') + ) { + console.log('isReady', isReady(elementToWatch, selector)); + if (isReady(elementToWatch, selector)) { + console.log('disconnecting'); + observer.disconnect(); + resolve(); + return; + } } } + }); + + observer.observe(root, globalOptions); + + // Set up an optional timeout to reject the promise if the element never gets child nodes + if (timeout > 0) { + setTimeout(() => { + observer.disconnect(); + reject(new Error(`Timeout waiting for ${selector}`)); + }, timeout); } }); +}; - observer.observe(root, { childList: true, attributes: true, subtree: true }); +const waitForElementChildren = createAwaitableMutationObserver({ + childList: true, + subtree: true, + isReady: (el: HTMLElement | null) => !!el?.childElementCount && el.childElementCount > 0, +}); - // Set up an optional timeout to reject the promise if the element never gets child nodes - if (timeout > 0) { - setTimeout(() => { - observer.disconnect(); - reject(new Error(`Timeout waiting for element children`)); - }, timeout); - } - }); -} +const waitForElementAttribute = createAwaitableMutationObserver({ + attributes: true, + // childList: true, + // subtree: true, + isReady: (el: HTMLElement | null, selector: string) => { + return el?.matches?.(selector) ?? false; + }, +}); /** * Detect when a Clerk component has mounted by watching DOM updates to an element with a `data-clerk-component="${component}"` property. @@ -84,12 +96,19 @@ export function useWaitForComponentMount( if (typeof window !== 'undefined' && !watcherRef.current) { const selector = `[data-clerk-component="${component}"]`; - const readySelector = options?.selector; - watcherRef.current = waitForElementChildren({ selector, readySelector }) + const attributeSelector = options?.selector; + console.log('attributeSelector', attributeSelector, attributeSelector + selector); + watcherRef.current = ( + attributeSelector + ? waitForElementAttribute({ selector: attributeSelector + selector }) + : waitForElementChildren({ selector }) + ) .then(() => { + console.log('rendered', component); setStatus('rendered'); }) .catch(() => { + console.log('error', component); setStatus('error'); }); } From e0fd38f0af6f1134cb70cc6dbd79f2625085b62d Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 26 Aug 2025 22:50:26 +0300 Subject: [PATCH 4/8] wip 2 --- .../src/utils/useWaitForComponentMount.ts | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/react/src/utils/useWaitForComponentMount.ts b/packages/react/src/utils/useWaitForComponentMount.ts index be319bfb147..5c07b9eebc5 100644 --- a/packages/react/src/utils/useWaitForComponentMount.ts +++ b/packages/react/src/utils/useWaitForComponentMount.ts @@ -9,7 +9,6 @@ const createAwaitableMutationObserver = ( return (options: { selector: string; root?: HTMLElement | null; timeout?: number }) => new Promise((resolve, reject) => { - console.log('Selector', options.selector); const { root = document?.body, selector, timeout = 0 } = options; if (!root) { @@ -31,19 +30,15 @@ const createAwaitableMutationObserver = ( // Set up a MutationObserver to detect when the element has children const observer = new MutationObserver(mutationsList => { for (const mutation of mutationsList) { - console.log('Mutation', mutation); if (!elementToWatch && selector) { elementToWatch = root?.querySelector(selector); } - console.log('elementToWatch', elementToWatch); if ( (globalOptions.childList && mutation.type === 'childList') || (globalOptions.attributes && mutation.type === 'attributes') ) { - console.log('isReady', isReady(elementToWatch, selector)); if (isReady(elementToWatch, selector)) { - console.log('disconnecting'); observer.disconnect(); resolve(); return; @@ -67,17 +62,17 @@ const createAwaitableMutationObserver = ( const waitForElementChildren = createAwaitableMutationObserver({ childList: true, subtree: true, - isReady: (el: HTMLElement | null) => !!el?.childElementCount && el.childElementCount > 0, + isReady: (el, selector) => !!el?.childElementCount && el?.matches?.(selector) && el.childElementCount > 0, }); -const waitForElementAttribute = createAwaitableMutationObserver({ - attributes: true, - // childList: true, - // subtree: true, - isReady: (el: HTMLElement | null, selector: string) => { - return el?.matches?.(selector) ?? false; - }, -}); +// const waitForElementAttribute = createAwaitableMutationObserver({ +// attributes: true, +// // childList: true, +// // subtree: true, +// isReady: (el: HTMLElement | null, selector: string) => { +// return el?.matches?.(selector) ?? false; +// }, +// }); /** * Detect when a Clerk component has mounted by watching DOM updates to an element with a `data-clerk-component="${component}"` property. @@ -98,11 +93,12 @@ export function useWaitForComponentMount( const selector = `[data-clerk-component="${component}"]`; const attributeSelector = options?.selector; console.log('attributeSelector', attributeSelector, attributeSelector + selector); - watcherRef.current = ( - attributeSelector - ? waitForElementAttribute({ selector: attributeSelector + selector }) - : waitForElementChildren({ selector }) - ) + watcherRef.current = waitForElementChildren({ + selector: attributeSelector ? attributeSelector + selector : selector, + }) + // .then(res => { + // return attributeSelector ? waitForElementAttribute({ selector: attributeSelector + selector }) : res; + // }) .then(() => { console.log('rendered', component); setStatus('rendered'); From 31115ef50bc2f9180f716073a986c91d49923c92 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 26 Aug 2025 23:05:25 +0300 Subject: [PATCH 5/8] final --- .../react/src/components/uiComponents.tsx | 1 - .../src/utils/useWaitForComponentMount.ts | 24 +++++-------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index 55ff15bca7f..256e81a2353 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -579,7 +579,6 @@ export const PricingTable = withClerk( ({ clerk, component, fallback, ...props }: WithClerkProp) => { const mountingStatus = useWaitForComponentMount(component, { selector: '[data-ready="true"]' }); const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; - console.log('shouldShowFallback', shouldShowFallback, mountingStatus); const rendererRootProps = { ...(shouldShowFallback && fallback && { style: { display: 'none' } }), diff --git a/packages/react/src/utils/useWaitForComponentMount.ts b/packages/react/src/utils/useWaitForComponentMount.ts index 5c07b9eebc5..2b60900ff3b 100644 --- a/packages/react/src/utils/useWaitForComponentMount.ts +++ b/packages/react/src/utils/useWaitForComponentMount.ts @@ -65,15 +65,6 @@ const waitForElementChildren = createAwaitableMutationObserver({ isReady: (el, selector) => !!el?.childElementCount && el?.matches?.(selector) && el.childElementCount > 0, }); -// const waitForElementAttribute = createAwaitableMutationObserver({ -// attributes: true, -// // childList: true, -// // subtree: true, -// isReady: (el: HTMLElement | null, selector: string) => { -// return el?.matches?.(selector) ?? false; -// }, -// }); - /** * Detect when a Clerk component has mounted by watching DOM updates to an element with a `data-clerk-component="${component}"` property. */ @@ -90,21 +81,18 @@ export function useWaitForComponentMount( } if (typeof window !== 'undefined' && !watcherRef.current) { - const selector = `[data-clerk-component="${component}"]`; - const attributeSelector = options?.selector; - console.log('attributeSelector', attributeSelector, attributeSelector + selector); + const defaultSelector = `[data-clerk-component="${component}"]`; + const selector = options?.selector; watcherRef.current = waitForElementChildren({ - selector: attributeSelector ? attributeSelector + selector : selector, + selector: selector + ? // Allows for `[data-clerk-component="xxxx"][data-some-attribute="123"] .my-class` + defaultSelector + selector + : defaultSelector, }) - // .then(res => { - // return attributeSelector ? waitForElementAttribute({ selector: attributeSelector + selector }) : res; - // }) .then(() => { - console.log('rendered', component); setStatus('rendered'); }) .catch(() => { - console.log('error', component); setStatus('error'); }); } From 399137db0b1f7dab75400da93a163fffc56c8fb7 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 26 Aug 2025 23:47:51 +0300 Subject: [PATCH 6/8] reactify the approach --- .../components/PricingTable/PricingTable.tsx | 23 +------------------ .../clerk-js/src/ui/customizables/Flow.tsx | 2 +- .../src/ui/elements/InvisibleRootBox.tsx | 11 ++++----- .../src/ui/elements/contexts/index.tsx | 13 ++--------- .../react/src/components/uiComponents.tsx | 6 ++++- 5 files changed, 14 insertions(+), 41 deletions(-) diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx index 9e85e66dd3d..24c0f739284 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx @@ -3,32 +3,11 @@ import type { CommercePlanResource, CommerceSubscriptionPlanPeriod, PricingTable import { useEffect, useMemo, useState } from 'react'; import { Flow } from '@/ui/customizables/Flow'; -import { useFlowMetadata } from '@/ui/elements/contexts'; import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscription } from '../../contexts'; import { PricingTableDefault } from './PricingTableDefault'; import { PricingTableMatrix } from './PricingTableMatrix'; -function SyncRootElement() { - const clerk = useClerk(); - const { data: subscription, isLoading: isSubscriptionLoading } = useSubscription(); - const { data: plans, isLoading: isPlansLoading } = usePlans(); - const { rootElement } = useFlowMetadata(); - - useEffect(() => { - if (isSubscriptionLoading || isPlansLoading) { - return; - } - const isReady = clerk.isSignedIn ? !!subscription : plans.length > 0; - - if (isReady) { - rootElement?.setAttribute('data-ready', 'true'); - } - }, [subscription, plans]); - - return null; -} - const PricingTableRoot = (props: PricingTableProps) => { const clerk = useClerk(); const { mode = 'mounted', signInMode = 'redirect' } = usePricingTableContext(); @@ -95,11 +74,11 @@ const PricingTableRoot = (props: PricingTableProps) => { return ( 0} sx={{ width: '100%', }} > - {mode !== 'modal' && (props as any).layout === 'matrix' ? ( { +const Root = (props: FlowRootProps & { isFlowReady?: boolean }) => { return ( diff --git a/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx b/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx index be1a6d25b1b..0562172b08e 100644 --- a/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx +++ b/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx @@ -1,13 +1,11 @@ import React from 'react'; import { makeCustomizable } from '../customizables/makeCustomizable'; -import { useFlowMetadata } from './contexts'; type RootBoxProps = React.PropsWithChildren<{ className: string }>; -const _InvisibleRootBox = React.memo((props: RootBoxProps) => { +const _InvisibleRootBox = React.memo((props: RootBoxProps & { isFlowReady?: boolean }) => { const [showSpan, setShowSpan] = React.useState(true); - const { setRootElement } = useFlowMetadata(); const parentRef = React.useRef(null); React.useEffect(() => { @@ -18,8 +16,10 @@ const _InvisibleRootBox = React.memo((props: RootBoxProps) => { if (showSpan) { setShowSpan(false); } - parent.className = props.className; - }, [props.className]); + + parent.setAttribute('class', props.className); + parent.setAttribute('data-component-status', props.isFlowReady ? 'ready' : 'awaiting-data'); + }, [props.className, props.isFlowReady]); return ( <> @@ -28,7 +28,6 @@ const _InvisibleRootBox = React.memo((props: RootBoxProps) => { { parentRef.current = el ? el.parentElement : parentRef.current; - setRootElement(parentRef.current); }} aria-hidden style={{ display: 'none' }} diff --git a/packages/clerk-js/src/ui/elements/contexts/index.tsx b/packages/clerk-js/src/ui/elements/contexts/index.tsx index 789b19e5326..b449c95a93b 100644 --- a/packages/clerk-js/src/ui/elements/contexts/index.tsx +++ b/packages/clerk-js/src/ui/elements/contexts/index.tsx @@ -125,20 +125,11 @@ export type FlowMetadata = { | 'accountSwitcher'; }; -const [FlowMetadataCtx, useFlowMetadata] = createContextAndHook< - FlowMetadata & { - rootElement: HTMLElement | null; - setRootElement: React.Dispatch>; - } ->('FlowMetadata'); +const [FlowMetadataCtx, useFlowMetadata] = createContextAndHook('FlowMetadata'); export const FlowMetadataProvider = (props: React.PropsWithChildren) => { const { flow, part } = props; - const [rootElement, setRootElement] = React.useState(null); - const value = React.useMemo( - () => ({ value: { ...props, rootElement, setRootElement } }), - [flow, part, rootElement, setRootElement], - ); + const value = React.useMemo(() => ({ value: { ...props } }), [flow, part]); return {props.children}; }; diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index 256e81a2353..b5307a66d1d 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -577,7 +577,11 @@ export const Waitlist = withClerk( export const PricingTable = withClerk( ({ clerk, component, fallback, ...props }: WithClerkProp) => { - const mountingStatus = useWaitForComponentMount(component, { selector: '[data-ready="true"]' }); + const mountingStatus = useWaitForComponentMount(component, { + // This attribute is added to the PricingTable root element after we've successfully fetched the plans asynchronously. + selector: '[data-component-status="ready"]', + }); + console.log('mountingStatus', mountingStatus); const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; const rendererRootProps = { From 08c8922ae0ea09715c93dce7fe15b27d3ab87c16 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 1 Sep 2025 18:45:47 +0300 Subject: [PATCH 7/8] Update packages/react/src/components/uiComponents.tsx Co-authored-by: Bryce Kalow --- packages/react/src/components/uiComponents.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index b5307a66d1d..cd0aa40cc09 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -581,7 +581,6 @@ export const PricingTable = withClerk( // This attribute is added to the PricingTable root element after we've successfully fetched the plans asynchronously. selector: '[data-component-status="ready"]', }); - console.log('mountingStatus', mountingStatus); const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; const rendererRootProps = { From 0650b55440171f3a328c9bb5388da3c9b7660d47 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 1 Sep 2025 18:46:32 +0300 Subject: [PATCH 8/8] Update packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx --- packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx b/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx index 0562172b08e..0cae2f62602 100644 --- a/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx +++ b/packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx @@ -26,9 +26,7 @@ const _InvisibleRootBox = React.memo((props: RootBoxProps & { isFlowReady?: bool {props.children} {showSpan && ( { - parentRef.current = el ? el.parentElement : parentRef.current; - }} + ref={el => (parentRef.current = el ? el.parentElement : parentRef.current)} aria-hidden style={{ display: 'none' }} />