From bc54be6121f5a3a0e98ba15682b097ed7de061e9 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 17 Jun 2024 22:51:57 +0300 Subject: [PATCH 1/6] feat(remix): Add support for Remix SPA Mode --- .changeset/empty-apples-kiss.md | 5 +++++ packages/remix/src/client/ClerkApp.tsx | 16 +++++++++++++--- packages/remix/src/client/RemixClerkProvider.tsx | 12 +++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 .changeset/empty-apples-kiss.md diff --git a/.changeset/empty-apples-kiss.md b/.changeset/empty-apples-kiss.md new file mode 100644 index 00000000000..c05df6bffaa --- /dev/null +++ b/.changeset/empty-apples-kiss.md @@ -0,0 +1,5 @@ +--- +"@clerk/remix": minor +--- + +Add support for Remix SPA Mode diff --git a/packages/remix/src/client/ClerkApp.tsx b/packages/remix/src/client/ClerkApp.tsx index a6946173ddb..f43747bd511 100644 --- a/packages/remix/src/client/ClerkApp.tsx +++ b/packages/remix/src/client/ClerkApp.tsx @@ -4,13 +4,23 @@ import React from 'react'; import { ClerkProvider } from './RemixClerkProvider'; import type { RemixClerkProviderProps } from './types'; +type SpaModeOptions = { spaMode: true; publishableKey: string } | { spaMode?: false; publishableKey?: string }; + type ClerkAppOptions = Partial< - Omit ->; + Omit +> & + SpaModeOptions; export function ClerkApp(App: () => JSX.Element, opts: ClerkAppOptions = {}) { return () => { - const { clerkState } = useLoaderData(); + let clerkState; + + // Don't use `useLoaderData` to fetch the clerk state if we're in SPA mode + if (!opts?.spaMode) { + const loaderData = useLoaderData<{ clerkState: any }>(); + clerkState = loaderData.clerkState; + } + return ( | */ type ClerkProviderPropsWithState = RemixClerkProviderProps & { clerkState: ClerkState; + spaMode?: boolean; }; export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState): JSX.Element { @@ -42,10 +43,13 @@ export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState awaitableNavigateRef.current = awaitableNavigate; }, [awaitableNavigate]); - const { clerkState, ...restProps } = rest; + const { clerkState, spaMode, ...restProps } = rest; ReactClerkProvider.displayName = 'ReactClerkProvider'; - assertValidClerkState(clerkState); + if (!spaMode) { + assertValidClerkState(clerkState); + } + const { __clerk_ssr_state, __publishableKey, @@ -68,7 +72,9 @@ export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState } = clerkState?.__internal_clerk_state || {}; React.useEffect(() => { - warnForSsr(clerkState); + if (!spaMode) { + warnForSsr(clerkState); + } }, []); React.useEffect(() => { From b763f073c81c415aff4ddc48b1d0e986e67fdbfb Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 27 Jun 2024 15:25:59 +0300 Subject: [PATCH 2/6] Use the window info to determine the SPA Mode --- packages/remix/src/client/ClerkApp.tsx | 15 +++++++++------ packages/remix/src/client/RemixClerkProvider.tsx | 14 +++++++------- packages/remix/src/client/types.ts | 8 ++++++++ packages/remix/src/utils/errors.ts | 11 +++++++++++ packages/remix/src/utils/utils.ts | 8 +++++++- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/remix/src/client/ClerkApp.tsx b/packages/remix/src/client/ClerkApp.tsx index f43747bd511..81d9411bc0d 100644 --- a/packages/remix/src/client/ClerkApp.tsx +++ b/packages/remix/src/client/ClerkApp.tsx @@ -1,26 +1,29 @@ import { useLoaderData } from '@remix-run/react'; import React from 'react'; +import { assertPublishableKeyInSpaMode } from '../utils'; import { ClerkProvider } from './RemixClerkProvider'; import type { RemixClerkProviderProps } from './types'; -type SpaModeOptions = { spaMode: true; publishableKey: string } | { spaMode?: false; publishableKey?: string }; - type ClerkAppOptions = Partial< - Omit -> & - SpaModeOptions; + Omit +>; export function ClerkApp(App: () => JSX.Element, opts: ClerkAppOptions = {}) { return () => { let clerkState; + const isSpaMode = window.__remixContext?.isSpaMode; // Don't use `useLoaderData` to fetch the clerk state if we're in SPA mode - if (!opts?.spaMode) { + if (!isSpaMode) { const loaderData = useLoaderData<{ clerkState: any }>(); clerkState = loaderData.clerkState; } + if (isSpaMode) { + assertPublishableKeyInSpaMode(opts.publishableKey); + } + return ( | */ type ClerkProviderPropsWithState = RemixClerkProviderProps & { clerkState: ClerkState; - spaMode?: boolean; }; export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState): JSX.Element { const awaitableNavigate = useAwaitableNavigate(); + const isSpaMode = window.__remixContext?.isSpaMode; React.useEffect(() => { awaitableNavigateRef.current = awaitableNavigate; }, [awaitableNavigate]); - const { clerkState, spaMode, ...restProps } = rest; + const { clerkState, ...restProps } = rest; ReactClerkProvider.displayName = 'ReactClerkProvider'; - if (!spaMode) { + if (!isSpaMode) { assertValidClerkState(clerkState); } @@ -72,7 +72,7 @@ export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState } = clerkState?.__internal_clerk_state || {}; React.useEffect(() => { - if (!spaMode) { + if (!isSpaMode) { warnForSsr(clerkState); } }, []); @@ -82,9 +82,9 @@ export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState }, []); const mergedProps = { - publishableKey: __publishableKey as any, - proxyUrl: __proxyUrl as any, - domain: __domain as any, + publishableKey: __publishableKey, + proxyUrl: __proxyUrl, + domain: __domain, isSatellite: __isSatellite, signInUrl: __signInUrl, signUpUrl: __signUpUrl, diff --git a/packages/remix/src/client/types.ts b/packages/remix/src/client/types.ts index ef4f1ed1770..4a4eedbfb9e 100644 --- a/packages/remix/src/client/types.ts +++ b/packages/remix/src/client/types.ts @@ -39,3 +39,11 @@ export type RemixClerkProviderProps = Without }; // https://remix.run/blog/remix-vite-stable#cloudflare-pages-support From e39e3a57a39607188dc1b6ba096c80609e23d44d Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 27 Jun 2024 15:34:27 +0300 Subject: [PATCH 3/6] Fix lint error --- packages/remix/src/client/RemixClerkProvider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/remix/src/client/RemixClerkProvider.tsx b/packages/remix/src/client/RemixClerkProvider.tsx index 4626e0a400b..966b83ace27 100644 --- a/packages/remix/src/client/RemixClerkProvider.tsx +++ b/packages/remix/src/client/RemixClerkProvider.tsx @@ -82,9 +82,9 @@ export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState }, []); const mergedProps = { - publishableKey: __publishableKey, - proxyUrl: __proxyUrl, - domain: __domain, + publishableKey: __publishableKey as any, + proxyUrl: __proxyUrl as any, + domain: __domain as any, isSatellite: __isSatellite, signInUrl: __signInUrl, signUpUrl: __signUpUrl, From df60950cdfd2502daf5c669660de30e9c58d9719 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 27 Jun 2024 15:52:02 +0300 Subject: [PATCH 4/6] Access window only when it's available --- packages/remix/src/client/ClerkApp.tsx | 4 ++-- packages/remix/src/client/RemixClerkProvider.tsx | 4 ++-- packages/remix/src/utils/utils.ts | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/remix/src/client/ClerkApp.tsx b/packages/remix/src/client/ClerkApp.tsx index 81d9411bc0d..747bcf7b6c2 100644 --- a/packages/remix/src/client/ClerkApp.tsx +++ b/packages/remix/src/client/ClerkApp.tsx @@ -1,7 +1,7 @@ import { useLoaderData } from '@remix-run/react'; import React from 'react'; -import { assertPublishableKeyInSpaMode } from '../utils'; +import { assertPublishableKeyInSpaMode, getSpaMode } from '../utils'; import { ClerkProvider } from './RemixClerkProvider'; import type { RemixClerkProviderProps } from './types'; @@ -12,7 +12,7 @@ type ClerkAppOptions = Partial< export function ClerkApp(App: () => JSX.Element, opts: ClerkAppOptions = {}) { return () => { let clerkState; - const isSpaMode = window.__remixContext?.isSpaMode; + const isSpaMode = getSpaMode(); // Don't use `useLoaderData` to fetch the clerk state if we're in SPA mode if (!isSpaMode) { diff --git a/packages/remix/src/client/RemixClerkProvider.tsx b/packages/remix/src/client/RemixClerkProvider.tsx index 966b83ace27..33c5c7712bd 100644 --- a/packages/remix/src/client/RemixClerkProvider.tsx +++ b/packages/remix/src/client/RemixClerkProvider.tsx @@ -1,7 +1,7 @@ import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; import React from 'react'; -import { assertValidClerkState, warnForSsr } from '../utils'; +import { assertValidClerkState, getSpaMode, warnForSsr } from '../utils'; import { ClerkRemixOptionsProvider } from './RemixOptionsContext'; import type { ClerkState, RemixClerkProviderProps } from './types'; import { useAwaitableNavigate } from './useAwaitableNavigate'; @@ -37,7 +37,7 @@ type ClerkProviderPropsWithState = RemixClerkProviderProps & { export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState): JSX.Element { const awaitableNavigate = useAwaitableNavigate(); - const isSpaMode = window.__remixContext?.isSpaMode; + const isSpaMode = getSpaMode(); React.useEffect(() => { awaitableNavigateRef.current = awaitableNavigate; diff --git a/packages/remix/src/utils/utils.ts b/packages/remix/src/utils/utils.ts index c8e18d21369..4561dec1641 100644 --- a/packages/remix/src/utils/utils.ts +++ b/packages/remix/src/utils/utils.ts @@ -79,3 +79,10 @@ export const getEnvVariable = (name: string, context: AppLoadContext | undefined return ''; }; + +export const getSpaMode = (): boolean => { + if (typeof window !== 'undefined' && typeof window.__remixContext?.isSpaMode !== 'undefined') { + return window.__remixContext.isSpaMode; + } + return false; +}; From 8307120ddbd198970741296d66a31d4a100a3721 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 27 Jun 2024 16:06:59 +0300 Subject: [PATCH 5/6] Fix wording --- packages/remix/src/utils/errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix/src/utils/errors.ts b/packages/remix/src/utils/errors.ts index 32b9ad022cd..95b7258e4cb 100644 --- a/packages/remix/src/utils/errors.ts +++ b/packages/remix/src/utils/errors.ts @@ -85,7 +85,7 @@ Check if signInUrl is missing from your configuration or if it is not an absolut export const publishableKeyMissingErrorInSpaMode = createErrorMessage(` You're trying to use Clerk in Remix SPA Mode without providing a Publishable Key. -Please provide a publishableKey in the options of the ClerkApp component. +Please provide the publishableKey option on the ClerkApp component. Example: From 3f654bc902e2ed7babc12ecef73115357b4136f6 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Fri, 28 Jun 2024 14:06:19 +0300 Subject: [PATCH 6/6] Change `getSpaMode` to `inSpaMode` --- packages/remix/src/client/ClerkApp.tsx | 4 ++-- packages/remix/src/client/RemixClerkProvider.tsx | 4 ++-- packages/remix/src/utils/utils.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/remix/src/client/ClerkApp.tsx b/packages/remix/src/client/ClerkApp.tsx index 747bcf7b6c2..a0a397df8b1 100644 --- a/packages/remix/src/client/ClerkApp.tsx +++ b/packages/remix/src/client/ClerkApp.tsx @@ -1,7 +1,7 @@ import { useLoaderData } from '@remix-run/react'; import React from 'react'; -import { assertPublishableKeyInSpaMode, getSpaMode } from '../utils'; +import { assertPublishableKeyInSpaMode, inSpaMode } from '../utils'; import { ClerkProvider } from './RemixClerkProvider'; import type { RemixClerkProviderProps } from './types'; @@ -12,7 +12,7 @@ type ClerkAppOptions = Partial< export function ClerkApp(App: () => JSX.Element, opts: ClerkAppOptions = {}) { return () => { let clerkState; - const isSpaMode = getSpaMode(); + const isSpaMode = inSpaMode(); // Don't use `useLoaderData` to fetch the clerk state if we're in SPA mode if (!isSpaMode) { diff --git a/packages/remix/src/client/RemixClerkProvider.tsx b/packages/remix/src/client/RemixClerkProvider.tsx index 33c5c7712bd..cd349c2faa4 100644 --- a/packages/remix/src/client/RemixClerkProvider.tsx +++ b/packages/remix/src/client/RemixClerkProvider.tsx @@ -1,7 +1,7 @@ import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; import React from 'react'; -import { assertValidClerkState, getSpaMode, warnForSsr } from '../utils'; +import { assertValidClerkState, inSpaMode, warnForSsr } from '../utils'; import { ClerkRemixOptionsProvider } from './RemixOptionsContext'; import type { ClerkState, RemixClerkProviderProps } from './types'; import { useAwaitableNavigate } from './useAwaitableNavigate'; @@ -37,7 +37,7 @@ type ClerkProviderPropsWithState = RemixClerkProviderProps & { export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState): JSX.Element { const awaitableNavigate = useAwaitableNavigate(); - const isSpaMode = getSpaMode(); + const isSpaMode = inSpaMode(); React.useEffect(() => { awaitableNavigateRef.current = awaitableNavigate; diff --git a/packages/remix/src/utils/utils.ts b/packages/remix/src/utils/utils.ts index 4561dec1641..aef25908fc1 100644 --- a/packages/remix/src/utils/utils.ts +++ b/packages/remix/src/utils/utils.ts @@ -80,7 +80,7 @@ export const getEnvVariable = (name: string, context: AppLoadContext | undefined return ''; }; -export const getSpaMode = (): boolean => { +export const inSpaMode = (): boolean => { if (typeof window !== 'undefined' && typeof window.__remixContext?.isSpaMode !== 'undefined') { return window.__remixContext.isSpaMode; }