diff --git a/.changeset/lemon-news-agree.md b/.changeset/lemon-news-agree.md
new file mode 100644
index 00000000000..a5ddafb63de
--- /dev/null
+++ b/.changeset/lemon-news-agree.md
@@ -0,0 +1,5 @@
+---
+'@clerk/react-router': patch
+---
+
+Improve environment variable loading for certain values
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 66a97afbb43..f349e00e101 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -160,7 +160,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- test-name: [ 'generic', 'express', 'quickstart', 'ap-flows', 'elements', 'sessions', 'astro', 'expo-web', 'tanstack-start', 'tanstack-router', 'vue', 'nuxt']
+ test-name: [ 'generic', 'express', 'quickstart', 'ap-flows', 'elements', 'sessions', 'astro', 'expo-web', 'tanstack-start', 'tanstack-router', 'vue', 'nuxt', 'react-router']
test-project: ['chrome']
include:
- test-name: 'nextjs'
diff --git a/integration/templates/react-router-node/app/routes/protected.tsx b/integration/templates/react-router-node/app/routes/protected.tsx
index e6267def3ac..2fdc2718e1c 100644
--- a/integration/templates/react-router-node/app/routes/protected.tsx
+++ b/integration/templates/react-router-node/app/routes/protected.tsx
@@ -1,21 +1,32 @@
import { redirect } from 'react-router';
+import { UserProfile } from '@clerk/react-router';
import { getAuth } from '@clerk/react-router/ssr.server';
+import { createClerkClient } from '@clerk/react-router/api.server';
import type { Route } from './+types/profile';
export async function loader(args: Route.LoaderArgs) {
const { userId } = await getAuth(args);
if (!userId) {
- return redirect('/sign-in?redirect_url=' + args.request.url);
+ return redirect('/sign-in');
}
- return {};
+ const user = await createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }).users.getUser(userId);
+
+ return {
+ user,
+ };
}
-export default function Profile(args: Route.ComponentProps) {
+export default function Profile({ loaderData }: Route.ComponentProps) {
return (
Protected
+
+
+ - First name: {loaderData.user.firstName}
+ - Email: {loaderData.user.emailAddresses[0].emailAddress}
+
);
}
diff --git a/integration/tests/react-router/basic.test.ts b/integration/tests/react-router/basic.test.ts
index dbd22816d65..595a724304b 100644
--- a/integration/tests/react-router/basic.test.ts
+++ b/integration/tests/react-router/basic.test.ts
@@ -1,4 +1,4 @@
-import { test } from '@playwright/test';
+import { expect, test } from '@playwright/test';
import { appConfigs } from '../../presets';
import type { FakeUser } from '../../testUtils';
@@ -49,5 +49,44 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern:
await u.po.userButton.toHaveVisibleMenuItems([/Manage account/i, /Sign out$/i]);
});
+
+ test('redirects to sign-in when unauthenticated', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await u.page.goToRelative('/protected');
+ await u.page.waitForURL(`${app.serverUrl}/sign-in`);
+ await u.po.signIn.waitForMounted();
+ });
+
+ test('renders control components contents', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await u.page.goToAppHome();
+ await expect(u.page.getByText('SignedOut')).toBeVisible();
+
+ await u.page.goToRelative('/sign-in');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+ await expect(u.page.getByText('SignedIn')).toBeVisible();
+ });
+
+ test('renders user profile with SSR data', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await u.page.goToRelative('/sign-in');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+
+ await u.po.userButton.waitForMounted();
+ await u.page.goToRelative('/protected');
+ await u.po.userProfile.waitForMounted();
+
+ // Fetched from an API endpoint (/api/me), which is server-rendered.
+ // This also verifies that the server middleware is working.
+ await expect(u.page.getByText(`First name: ${fakeUser.firstName}`)).toBeVisible();
+ await expect(u.page.getByText(`Email: ${fakeUser.email}`)).toBeVisible();
+ });
},
);
diff --git a/packages/react-router/src/client/ReactRouterClerkProvider.tsx b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
index f6b983b5401..e80bb6a7eb9 100644
--- a/packages/react-router/src/client/ReactRouterClerkProvider.tsx
+++ b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
@@ -32,7 +32,7 @@ type ClerkProviderPropsWithState = ReactRouterClerkProviderProps & {
clerkState?: ClerkState;
};
-function ClerkProviderBase({ children, ...rest }: ClerkProviderPropsWithState): JSX.Element {
+function ClerkProviderBase({ children, ...rest }: ClerkProviderPropsWithState) {
const awaitableNavigate = useAwaitableNavigate();
const isSpaMode = _isSpaMode();
diff --git a/packages/react-router/src/client/types.ts b/packages/react-router/src/client/types.ts
index 7a8c4ad8bea..65020edbd71 100644
--- a/packages/react-router/src/client/types.ts
+++ b/packages/react-router/src/client/types.ts
@@ -36,8 +36,9 @@ export type WithClerkState = {
export type ReactRouterClerkProviderProps = Without & {
/**
- * Used to override the default CLERK_PUBLISHABLE_KEY env variable if needed.
- * This is optional for React Router as the ClerkProvider will automatically use the CLERK_PUBLISHABLE_KEY env variable if it exists.
+ * Used to override the default VITE_CLERK_PUBLISHABLE_KEY env variable if needed.
+ * This is optional for React Router (in SSR mode) as the ClerkProvider will automatically use the VITE_CLERK_PUBLISHABLE_KEY env variable if it exists.
+ * If you use React Router in SPA mode or as a library, you have to pass the publishableKey prop.
*/
publishableKey?: string;
children: React.ReactNode;
diff --git a/packages/react-router/src/ssr/loadOptions.ts b/packages/react-router/src/ssr/loadOptions.ts
index 8bcd0ed44eb..79a15dd0113 100644
--- a/packages/react-router/src/ssr/loadOptions.ts
+++ b/packages/react-router/src/ssr/loadOptions.ts
@@ -13,12 +13,13 @@ export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderO
const { request, context } = args;
const clerkRequest = createClerkRequest(patchRequest(request));
- // Fetch environment variables across Remix runtime.
- // 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
+ // Fetch environment variables across React Router runtime.
+ // 1. First check if the user passed the key in the getAuth() function or the rootAuthLoader().
// 2. Then try from process.env if exists (Node).
- // 3. Then try from globalThis (Cloudflare Workers).
- // 4. Then from loader context (Cloudflare Pages).
- const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
+ // 3. Then try from import.meta.env if exists (Vite).
+ // 4. Then try from globalThis (Cloudflare Workers).
+ // 5. Then from loader context (Cloudflare Pages).
+ const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context);
const publishableKey = overrides.publishableKey || getPublicEnvVariables(context).publishableKey;
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
@@ -33,13 +34,13 @@ export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderO
const signInUrl = overrides.signInUrl || getPublicEnvVariables(context).signInUrl;
const signUpUrl = overrides.signUpUrl || getPublicEnvVariables(context).signUpUrl;
const signInForceRedirectUrl =
- overrides.signInForceRedirectUrl || getEnvVariable('CLERK_SIGN_IN_FORCE_REDIRECT_URL', context) || '';
+ overrides.signInForceRedirectUrl || getPublicEnvVariables(context).signInForceRedirectUrl;
const signUpForceRedirectUrl =
- overrides.signUpForceRedirectUrl || getEnvVariable('CLERK_SIGN_UP_FORCE_REDIRECT_URL', context) || '';
+ overrides.signUpForceRedirectUrl || getPublicEnvVariables(context).signUpForceRedirectUrl;
const signInFallbackRedirectUrl =
- overrides.signInFallbackRedirectUrl || getEnvVariable('CLERK_SIGN_IN_FALLBACK_REDIRECT_URL', context) || '';
+ overrides.signInFallbackRedirectUrl || getPublicEnvVariables(context).signInFallbackRedirectUrl;
const signUpFallbackRedirectUrl =
- overrides.signUpFallbackRedirectUrl || getEnvVariable('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', context) || '';
+ overrides.signUpFallbackRedirectUrl || getPublicEnvVariables(context).signUpFallbackRedirectUrl;
const afterSignInUrl = overrides.afterSignInUrl || getPublicEnvVariables(context).afterSignInUrl;
const afterSignUpUrl = overrides.afterSignUpUrl || getPublicEnvVariables(context).afterSignUpUrl;
diff --git a/packages/react-router/src/ssr/types.ts b/packages/react-router/src/ssr/types.ts
index f130ecaa38a..10b8a9f855d 100644
--- a/packages/react-router/src/ssr/types.ts
+++ b/packages/react-router/src/ssr/types.ts
@@ -41,8 +41,17 @@ export type RouteInfo = {
export type GetAuthReturn = Promise;
export type RootAuthLoaderOptions = {
+ /**
+ * Used to override the default VITE_CLERK_PUBLISHABLE_KEY env variable if needed.
+ */
publishableKey?: string;
+ /**
+ * Used to override the CLERK_JWT_KEY env variable if needed.
+ */
jwtKey?: string;
+ /**
+ * Used to override the CLERK_SECRET_KEY env variable if needed.
+ */
secretKey?: string;
/**
* @deprecated This option will be removed in the next major version.
diff --git a/packages/react-router/src/utils/env.ts b/packages/react-router/src/utils/env.ts
index 92fb66a2178..d42c7a0831b 100644
--- a/packages/react-router/src/utils/env.ts
+++ b/packages/react-router/src/utils/env.ts
@@ -75,6 +75,18 @@ export const getPublicEnvVariables = (context: AppLoadContext | undefined) => {
telemetryDebug:
isTruthy(getEnvVariable('VITE_CLERK_TELEMETRY_DEBUG', context)) ||
isTruthy(getEnvVariable('CLERK_TELEMETRY_DEBUG', context)),
+ signInForceRedirectUrl:
+ getEnvVariable('VITE_CLERK_SIGN_IN_FORCE_REDIRECT_URL', context) ||
+ getEnvVariable('CLERK_SIGN_IN_FORCE_REDIRECT_URL', context),
+ signUpForceRedirectUrl:
+ getEnvVariable('VITE_CLERK_SIGN_UP_FORCE_REDIRECT_URL', context) ||
+ getEnvVariable('CLERK_SIGN_UP_FORCE_REDIRECT_URL', context),
+ signInFallbackRedirectUrl:
+ getEnvVariable('VITE_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL', context) ||
+ getEnvVariable('CLERK_SIGN_IN_FALLBACK_REDIRECT_URL', context),
+ signUpFallbackRedirectUrl:
+ getEnvVariable('VITE_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', context) ||
+ getEnvVariable('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', context),
afterSignInUrl:
getEnvVariable('VITE_CLERK_AFTER_SIGN_IN_URL', context) || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context),
afterSignUpUrl:
diff --git a/packages/react-router/src/utils/errors.ts b/packages/react-router/src/utils/errors.ts
index 14c7aac65fb..8e0a7682f2b 100644
--- a/packages/react-router/src/utils/errors.ts
+++ b/packages/react-router/src/utils/errors.ts
@@ -3,7 +3,6 @@ const createErrorMessage = (msg: string) => {
For more info, check out the docs: https://clerk.com/docs,
or come say hi in our discord server: https://clerk.com/discord
-
`;
};