Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
Comment on lines +84 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anagstef could you add a link where window.__remixContext.isSpaMode is mentioned in their docs ? I couldn't find it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@panteliselef It's not mentioned because it is kinda internal. My initial proposal was to introduce a spaMode: true prop option, but discussed with @octoper and this solution has better DX.

return false;
};