Skip to content

Commit

Permalink
feat(remix): Add support for Remix SPA Mode (#3580)
Browse files Browse the repository at this point in the history
  • Loading branch information
anagstef authored Jun 28, 2024
1 parent 3dc5b3d commit 2f8e79e
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-apples-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/remix": minor
---

Add support for Remix SPA Mode
15 changes: 14 additions & 1 deletion packages/remix/src/client/ClerkApp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useLoaderData } from '@remix-run/react';
import React from 'react';

import { assertPublishableKeyInSpaMode, inSpaMode } from '../utils';
import { ClerkProvider } from './RemixClerkProvider';
import type { RemixClerkProviderProps } from './types';

Expand All @@ -10,7 +11,19 @@ type ClerkAppOptions = Partial<

export function ClerkApp(App: () => JSX.Element, opts: ClerkAppOptions = {}) {
return () => {
const { clerkState } = useLoaderData();
let clerkState;
const isSpaMode = inSpaMode();

// Don't use `useLoaderData` to fetch the clerk state if we're in SPA mode
if (!isSpaMode) {
const loaderData = useLoaderData<{ clerkState: any }>();
clerkState = loaderData.clerkState;
}

if (isSpaMode) {
assertPublishableKeyInSpaMode(opts.publishableKey);
}

return (
<ClerkProvider
/* @ts-ignore The type of opts cannot be inferred by TS automatically because of the complex
Expand Down
12 changes: 9 additions & 3 deletions packages/remix/src/client/RemixClerkProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react';
import React from 'react';

import { assertValidClerkState, warnForSsr } from '../utils';
import { assertValidClerkState, inSpaMode, warnForSsr } from '../utils';
import { ClerkRemixOptionsProvider } from './RemixOptionsContext';
import type { ClerkState, RemixClerkProviderProps } from './types';
import { useAwaitableNavigate } from './useAwaitableNavigate';
Expand Down Expand Up @@ -37,6 +37,7 @@ type ClerkProviderPropsWithState = RemixClerkProviderProps & {

export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState): JSX.Element {
const awaitableNavigate = useAwaitableNavigate();
const isSpaMode = inSpaMode();

React.useEffect(() => {
awaitableNavigateRef.current = awaitableNavigate;
Expand All @@ -45,7 +46,10 @@ export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState
const { clerkState, ...restProps } = rest;
ReactClerkProvider.displayName = 'ReactClerkProvider';

assertValidClerkState(clerkState);
if (!isSpaMode) {
assertValidClerkState(clerkState);
}

const {
__clerk_ssr_state,
__publishableKey,
Expand All @@ -68,7 +72,9 @@ export function ClerkProvider({ children, ...rest }: ClerkProviderPropsWithState
} = clerkState?.__internal_clerk_state || {};

React.useEffect(() => {
warnForSsr(clerkState);
if (!isSpaMode) {
warnForSsr(clerkState);
}
}, []);

React.useEffect(() => {
Expand Down
8 changes: 8 additions & 0 deletions packages/remix/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@ export type RemixClerkProviderProps = Without<ClerkProviderProps, 'publishableKe
publishableKey?: string;
children: React.ReactNode;
};

declare global {
interface Window {
__remixContext: {
isSpaMode?: boolean;
};
}
}
11 changes: 11 additions & 0 deletions packages/remix/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ export const satelliteAndMissingProxyUrlAndDomain = createErrorMessage(
export const satelliteAndMissingSignInUrl = createErrorMessage(`
Invalid signInUrl. A satellite application requires a signInUrl for development instances.
Check if signInUrl is missing from your configuration or if it is not an absolute URL.`);

export const publishableKeyMissingErrorInSpaMode = createErrorMessage(`
You're trying to use Clerk in Remix SPA Mode without providing a Publishable Key.
Please provide the publishableKey option on the ClerkApp component.
Example:
export default ClerkApp(App, {
publishableKey: 'pk_test_XXX'
});
`);
15 changes: 14 additions & 1 deletion packages/remix/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AppLoadContext } from '@remix-run/server-runtime';

import type { ClerkState } from '../client/types';
import { invalidClerkStatePropError, noClerkStateError } from './errors';
import { invalidClerkStatePropError, noClerkStateError, publishableKeyMissingErrorInSpaMode } from './errors';

export function warnForSsr(val: ClerkState | undefined) {
if (!val || !val.__internal_clerk_state) {
Expand All @@ -24,6 +24,12 @@ export function assertValidClerkState(val: any): asserts val is ClerkState | und
}
}

export function assertPublishableKeyInSpaMode(key: any): asserts key is string {
if (!key || typeof key !== 'string') {
throw new Error(publishableKeyMissingErrorInSpaMode);
}
}

type CloudflareEnv = { env: Record<string, string> };

// https://remix.run/blog/remix-vite-stable#cloudflare-pages-support
Expand Down Expand Up @@ -73,3 +79,10 @@ export const getEnvVariable = (name: string, context: AppLoadContext | undefined

return '';
};

export const inSpaMode = (): boolean => {
if (typeof window !== 'undefined' && typeof window.__remixContext?.isSpaMode !== 'undefined') {
return window.__remixContext.isSpaMode;
}
return false;
};

0 comments on commit 2f8e79e

Please sign in to comment.