) => {
+ const mountingStatus = useWaitForComponentMount(component);
+ const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded;
+
+ const rendererRootProps = {
+ ...(shouldShowFallback && fallback && { style: { display: 'none' } }),
+ };
+
+ return (
+ <>
+ {shouldShowFallback && fallback}
+ {clerk.loaded && (
+
+ )}
+ >
+ );
+ },
+ { component: 'Waitlist', renderWhileLoading: true },
+);
diff --git a/packages/react/src/components/withClerk.tsx b/packages/react/src/components/withClerk.tsx
index 54ff7be47c..a78a1c82c8 100644
--- a/packages/react/src/components/withClerk.tsx
+++ b/packages/react/src/components/withClerk.tsx
@@ -6,24 +6,30 @@ import { errorThrower } from '../errors/errorThrower';
import { hocChildrenNotAFunctionError } from '../errors/messages';
import { useAssertWrappedByClerkProvider } from '../hooks/useAssertWrappedByClerkProvider';
-export const withClerk = (
+export const withClerk =
(
Component: React.ComponentType
,
- displayName?: string,
+ displayNameOrOptions?: string | { component: string; renderWhileLoading?: boolean },
) => {
- displayName = displayName || Component.displayName || Component.name || 'Component';
+ const passedDisplayedName =
+ typeof displayNameOrOptions === 'string' ? displayNameOrOptions : displayNameOrOptions?.component;
+ const displayName = passedDisplayedName || Component.displayName || Component.name || 'Component';
Component.displayName = displayName;
+
+ const options = typeof displayNameOrOptions === 'string' ? undefined : displayNameOrOptions;
+
const HOC = (props: Without
) => {
useAssertWrappedByClerkProvider(displayName || 'withClerk');
const clerk = useIsomorphicClerkContext();
- if (!clerk.loaded) {
+ if (!clerk.loaded && !options?.renderWhileLoading) {
return null;
}
return (
);
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index eb1326d8d7..0c732dbc1c 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -70,7 +70,7 @@ export interface HeadlessBrowserClerkConstructor {
new (publishableKey: string, options?: DomainOrProxyUrl): HeadlessBrowserClerk;
}
-export type WithClerkProp = T & { clerk: LoadedClerk };
+export type WithClerkProp = T & { clerk: LoadedClerk; component?: string };
export interface CustomPortalsRendererProps {
customPagesPortals?: any[];
diff --git a/packages/react/src/utils/useWaitForComponentMount.ts b/packages/react/src/utils/useWaitForComponentMount.ts
new file mode 100644
index 0000000000..dc6019b65f
--- /dev/null
+++ b/packages/react/src/utils/useWaitForComponentMount.ts
@@ -0,0 +1,80 @@
+import { useEffect, useRef, useState } from 'react';
+
+/**
+ * Used to detect when a Clerk component has been added to the DOM.
+ */
+function waitForElementChildren(options: { selector?: string; root?: HTMLElement | null; timeout?: number }) {
+ const { root = document?.body, selector, timeout = 0 } = options;
+
+ return new Promise((resolve, reject) => {
+ if (!root) {
+ reject(new Error('No root element provided'));
+ return;
+ }
+
+ let elementToWatch: HTMLElement | null = root;
+ if (selector) {
+ elementToWatch = root?.querySelector(selector);
+ }
+
+ // Check if the element already has child nodes
+ const isElementAlreadyPresent = elementToWatch?.childElementCount && elementToWatch.childElementCount > 0;
+ if (isElementAlreadyPresent) {
+ resolve();
+ return;
+ }
+
+ // 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?.childElementCount && elementToWatch.childElementCount > 0) {
+ observer.disconnect();
+ resolve();
+ return;
+ }
+ }
+ }
+ });
+
+ observer.observe(root, { childList: true, subtree: true });
+
+ // 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);
+ }
+ });
+}
+
+/**
+ * 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) {
+ const watcherRef = useRef>();
+ const [status, setStatus] = useState<'rendering' | 'rendered' | 'error'>('rendering');
+
+ useEffect(() => {
+ if (!component) {
+ throw new Error('Clerk: no component name provided, unable to detect mount.');
+ }
+
+ if (typeof window !== 'undefined' && !watcherRef.current) {
+ watcherRef.current = waitForElementChildren({ selector: `[data-clerk-component="${component}"]` })
+ .then(() => {
+ setStatus('rendered');
+ })
+ .catch(() => {
+ setStatus('error');
+ });
+ }
+ }, [component]);
+
+ return status;
+}