Skip to content

Commit

Permalink
feat(astro): Introduce <ClerkLoaded/> and <ClerkLoading/> React c…
Browse files Browse the repository at this point in the history
…omponents (#3724)
  • Loading branch information
wobsoriano authored Jul 16, 2024
1 parent bd503bb commit 75e872b
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/thick-buttons-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/astro": patch
---

Introduce `<ClerkLoaded/>` and `<ClerkLoading/>` React components
13 changes: 13 additions & 0 deletions integration/templates/astro-node/src/pages/utility.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
import { ClerkLoaded, ClerkLoading } from '@clerk/astro/react';
import Layout from "../layouts/Layout.astro";
---

<Layout title="Utility">
<ClerkLoading client:load>
<div>Clerk is loading</div>
</ClerkLoading>
<ClerkLoaded client:load>
<div>Clerk is loaded</div>
</ClerkLoaded>
</Layout>
10 changes: 10 additions & 0 deletions integration/tests/astro/react/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,15 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })(
await expect(u.page.getByText('Go to this page to see your profile')).toBeVisible();
await expect(u.page.getByText('Sign out!')).toBeVisible();
});

test('render content based on Clerk loaded status', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/utility');
await expect(u.page.getByText('Clerk is loading')).toBeVisible();
await expect(u.page.getByText('Clerk is loaded')).toBeHidden();
await u.page.waitForClerkJsLoaded();
await expect(u.page.getByText('Clerk is loaded')).toBeVisible();
await expect(u.page.getByText('Clerk is loading')).toBeHidden();
});
},
);
45 changes: 44 additions & 1 deletion packages/astro/src/react/controlComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { CheckAuthorizationWithCustomPermissions, HandleOAuthCallbackParams } from '@clerk/types';
import { computed } from 'nanostores';
import type { PropsWithChildren } from 'react';
import React from 'react';
import React, { useEffect, useState } from 'react';

import { $csrState } from '../stores/internal';
import type { ProtectComponentDefaultProps } from '../types';
import { useAuth } from './hooks';
import type { WithClerkProp } from './utils';
Expand All @@ -24,6 +26,47 @@ export function SignedIn(props: PropsWithChildren) {
return props.children;
}

const $isLoadingClerkStore = computed($csrState, state => state.isLoaded);

/*
* This hook ensures that the Clerk loading state is always shown on the first render,
* preventing potential hydration mismatches and race conditions.
*
*/
const useForceFirstRenderValue = () => {
const [isLoaded, setIsLoaded] = useState(false);

useEffect(() => {
const unsub = $isLoadingClerkStore.subscribe(() => {
setIsLoaded(true);
});

return () => unsub();
}, []);

return isLoaded;
};

export const ClerkLoaded = ({ children }: React.PropsWithChildren<unknown>): JSX.Element | null => {
const isLoaded = useForceFirstRenderValue();

if (!isLoaded) {
return null;
}

return <>{children}</>;
};

export const ClerkLoading = ({ children }: React.PropsWithChildren<unknown>): JSX.Element | null => {
const isLoaded = useForceFirstRenderValue();

if (isLoaded) {
return null;
}

return <>{children}</>;
};

export type ProtectProps = React.PropsWithChildren<
ProtectComponentDefaultProps & {
fallback?: React.ReactNode;
Expand Down

0 comments on commit 75e872b

Please sign in to comment.