Skip to content

Commit 9796fbf

Browse files
authored
fix(clerk-js,clerk-react): Avoid CLS when fallback is passed to PricingTable (#6644)
1 parent 1fc24e3 commit 9796fbf

File tree

7 files changed

+82
-48
lines changed

7 files changed

+82
-48
lines changed

.changeset/floppy-glasses-share.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/clerk-react': patch
4+
---
5+
6+
Wait for pricing table data to be ready before hiding its fallback.

packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const PricingTableRoot = (props: PricingTableProps) => {
7474
return (
7575
<Flow.Root
7676
flow='pricingTable'
77+
isFlowReady={clerk.isSignedIn ? !!subscription : plans.length > 0}
7778
sx={{
7879
width: '100%',
7980
}}

packages/clerk-js/src/ui/customizables/Flow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { descriptors } from './index';
1010

1111
type FlowRootProps = React.PropsWithChildren & FlowMetadata & { sx?: ThemableCssProp };
1212

13-
const Root = (props: FlowRootProps) => {
13+
const Root = (props: FlowRootProps & { isFlowReady?: boolean }) => {
1414
return (
1515
<FlowMetadataProvider flow={props.flow}>
1616
<InternalThemeProvider>

packages/clerk-js/src/ui/elements/InvisibleRootBox.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { makeCustomizable } from '../customizables/makeCustomizable';
44

55
type RootBoxProps = React.PropsWithChildren<{ className: string }>;
66

7-
const _InvisibleRootBox = React.memo((props: RootBoxProps) => {
7+
const _InvisibleRootBox = React.memo((props: RootBoxProps & { isFlowReady?: boolean }) => {
88
const [showSpan, setShowSpan] = React.useState(true);
99
const parentRef = React.useRef<HTMLElement | null>(null);
1010

@@ -16,8 +16,10 @@ const _InvisibleRootBox = React.memo((props: RootBoxProps) => {
1616
if (showSpan) {
1717
setShowSpan(false);
1818
}
19-
parent.className = props.className;
20-
}, [props.className]);
19+
20+
parent.setAttribute('class', props.className);
21+
parent.setAttribute('data-component-status', props.isFlowReady ? 'ready' : 'awaiting-data');
22+
}, [props.className, props.isFlowReady]);
2123

2224
return (
2325
<>

packages/clerk-js/src/ui/elements/contexts/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const [FlowMetadataCtx, useFlowMetadata] = createContextAndHook<FlowMetadata>('F
129129

130130
export const FlowMetadataProvider = (props: React.PropsWithChildren<FlowMetadata>) => {
131131
const { flow, part } = props;
132-
const value = React.useMemo(() => ({ value: props }), [flow, part]);
132+
const value = React.useMemo(() => ({ value: { ...props } }), [flow, part]);
133133
return <FlowMetadataCtx.Provider value={value}>{props.children}</FlowMetadataCtx.Provider>;
134134
};
135135

packages/react/src/components/uiComponents.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,10 @@ export const Waitlist = withClerk(
577577

578578
export const PricingTable = withClerk(
579579
({ clerk, component, fallback, ...props }: WithClerkProp<PricingTableProps & FallbackProp>) => {
580-
const mountingStatus = useWaitForComponentMount(component);
580+
const mountingStatus = useWaitForComponentMount(component, {
581+
// This attribute is added to the PricingTable root element after we've successfully fetched the plans asynchronously.
582+
selector: '[data-component-status="ready"]',
583+
});
581584
const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded;
582585

583586
const rendererRootProps = {
Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,77 @@
11
import { useEffect, useRef, useState } from 'react';
22

3-
/**
4-
* Used to detect when a Clerk component has been added to the DOM.
5-
*/
6-
function waitForElementChildren(options: { selector?: string; root?: HTMLElement | null; timeout?: number }) {
7-
const { root = document?.body, selector, timeout = 0 } = options;
3+
const createAwaitableMutationObserver = (
4+
globalOptions: MutationObserverInit & {
5+
isReady: (el: HTMLElement | null, selector: string) => boolean;
6+
},
7+
) => {
8+
const isReady = globalOptions?.isReady;
89

9-
return new Promise<void>((resolve, reject) => {
10-
if (!root) {
11-
reject(new Error('No root element provided'));
12-
return;
13-
}
10+
return (options: { selector: string; root?: HTMLElement | null; timeout?: number }) =>
11+
new Promise<void>((resolve, reject) => {
12+
const { root = document?.body, selector, timeout = 0 } = options;
1413

15-
let elementToWatch: HTMLElement | null = root;
16-
if (selector) {
17-
elementToWatch = root?.querySelector(selector);
18-
}
14+
if (!root) {
15+
reject(new Error('No root element provided'));
16+
return;
17+
}
1918

20-
// Check if the element already has child nodes
21-
const isElementAlreadyPresent = elementToWatch?.childElementCount && elementToWatch.childElementCount > 0;
22-
if (isElementAlreadyPresent) {
23-
resolve();
24-
return;
25-
}
19+
let elementToWatch: HTMLElement | null = root;
20+
if (selector) {
21+
elementToWatch = root?.querySelector(selector);
22+
}
2623

27-
// Set up a MutationObserver to detect when the element has children
28-
const observer = new MutationObserver(mutationsList => {
29-
for (const mutation of mutationsList) {
30-
if (mutation.type === 'childList') {
24+
// Initial readiness check
25+
if (isReady(elementToWatch, selector)) {
26+
resolve();
27+
return;
28+
}
29+
30+
// Set up a MutationObserver to detect when the element has children
31+
const observer = new MutationObserver(mutationsList => {
32+
for (const mutation of mutationsList) {
3133
if (!elementToWatch && selector) {
3234
elementToWatch = root?.querySelector(selector);
3335
}
3436

35-
if (elementToWatch?.childElementCount && elementToWatch.childElementCount > 0) {
36-
observer.disconnect();
37-
resolve();
38-
return;
37+
if (
38+
(globalOptions.childList && mutation.type === 'childList') ||
39+
(globalOptions.attributes && mutation.type === 'attributes')
40+
) {
41+
if (isReady(elementToWatch, selector)) {
42+
observer.disconnect();
43+
resolve();
44+
return;
45+
}
3946
}
4047
}
48+
});
49+
50+
observer.observe(root, globalOptions);
51+
52+
// Set up an optional timeout to reject the promise if the element never gets child nodes
53+
if (timeout > 0) {
54+
setTimeout(() => {
55+
observer.disconnect();
56+
reject(new Error(`Timeout waiting for ${selector}`));
57+
}, timeout);
4158
}
4259
});
60+
};
4361

44-
observer.observe(root, { childList: true, subtree: true });
45-
46-
// Set up an optional timeout to reject the promise if the element never gets child nodes
47-
if (timeout > 0) {
48-
setTimeout(() => {
49-
observer.disconnect();
50-
reject(new Error(`Timeout waiting for element children`));
51-
}, timeout);
52-
}
53-
});
54-
}
62+
const waitForElementChildren = createAwaitableMutationObserver({
63+
childList: true,
64+
subtree: true,
65+
isReady: (el, selector) => !!el?.childElementCount && el?.matches?.(selector) && el.childElementCount > 0,
66+
});
5567

5668
/**
5769
* Detect when a Clerk component has mounted by watching DOM updates to an element with a `data-clerk-component="${component}"` property.
5870
*/
59-
export function useWaitForComponentMount(component?: string) {
71+
export function useWaitForComponentMount(
72+
component?: string,
73+
options?: { selector: string },
74+
): 'rendering' | 'rendered' | 'error' {
6075
const watcherRef = useRef<Promise<void>>();
6176
const [status, setStatus] = useState<'rendering' | 'rendered' | 'error'>('rendering');
6277

@@ -66,15 +81,22 @@ export function useWaitForComponentMount(component?: string) {
6681
}
6782

6883
if (typeof window !== 'undefined' && !watcherRef.current) {
69-
watcherRef.current = waitForElementChildren({ selector: `[data-clerk-component="${component}"]` })
84+
const defaultSelector = `[data-clerk-component="${component}"]`;
85+
const selector = options?.selector;
86+
watcherRef.current = waitForElementChildren({
87+
selector: selector
88+
? // Allows for `[data-clerk-component="xxxx"][data-some-attribute="123"] .my-class`
89+
defaultSelector + selector
90+
: defaultSelector,
91+
})
7092
.then(() => {
7193
setStatus('rendered');
7294
})
7395
.catch(() => {
7496
setStatus('error');
7597
});
7698
}
77-
}, [component]);
99+
}, [component, options?.selector]);
78100

79101
return status;
80102
}

0 commit comments

Comments
 (0)