Skip to content

Commit 37ad032

Browse files
authored
feat(remix): Support defer() usage in rootAuthLoader() (clerk#1926)
* feat(remix): Support defer() usage in rootAuthLoader() * chore(repo): Add changeset * fix(remix): Remove unused import * Update playground/remix-node/app/root.tsx
1 parent 52f8553 commit 37ad032

4 files changed

Lines changed: 81 additions & 17 deletions

File tree

.changeset/loud-peaches-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/remix': minor
3+
---
4+
5+
Support usage of Remix's `defer()` method in the loader passed to `rootAuthLoader()`.

packages/remix/src/ssr/rootAuthLoader.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { sanitizeAuthObject } from '@clerk/backend';
2+
import type { defer } from '@remix-run/server-runtime';
3+
import { isDeferredData } from '@remix-run/server-runtime/dist/responses';
24

35
import { invalidRootLoaderCallbackReturn } from '../errors';
46
import { authenticateRequest } from './authenticateRequest';
57
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
68
import {
79
assertValidHandlerResult,
810
injectAuthIntoRequest,
11+
injectRequestStateIntoDeferredData,
912
injectRequestStateIntoResponse,
1013
interstitialJsonResponse,
1114
isRedirect,
@@ -64,6 +67,16 @@ export const rootAuthLoader: RootAuthLoader = async (
6467
const handlerResult = await handler(injectAuthIntoRequest(args, sanitizeAuthObject(requestState.toAuth())));
6568
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);
6669

70+
// When using defer(), we need to inject the clerk auth state into its internal data object.
71+
if (isDeferredData(handlerResult)) {
72+
return injectRequestStateIntoDeferredData(
73+
// This is necessary because the DeferredData type is not exported from remix.
74+
handlerResult as unknown as ReturnType<typeof defer>,
75+
requestState,
76+
args.context,
77+
);
78+
}
79+
6780
if (isResponse(handlerResult)) {
6881
try {
6982
// respect and pass-through any redirects without modifying them

packages/remix/src/ssr/utils.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AuthObject, RequestState } from '@clerk/backend';
22
import { constants, debugRequestState, loadInterstitialFromLocal } from '@clerk/backend';
3-
import type { AppLoadContext } from '@remix-run/server-runtime';
3+
import type { AppLoadContext, defer } from '@remix-run/server-runtime';
44
import { json } from '@remix-run/server-runtime';
55
import cookie from 'cookie';
66

@@ -108,9 +108,51 @@ export const injectRequestStateIntoResponse = async (
108108
context: AppLoadContext,
109109
) => {
110110
// eslint-disable-next-line @typescript-eslint/no-unused-vars
111-
const { reason, message, isSignedIn, isInterstitial, ...rest } = requestState;
112111
const clone = response.clone();
113112
const data = await clone.json();
113+
114+
const { clerkState, headers } = getResponseClerkState(requestState, context);
115+
116+
// set the correct content-type header in case the user returned a `Response` directly
117+
// without setting the header, instead of using the `json()` helper
118+
clone.headers.set(constants.Headers.ContentType, constants.ContentTypes.Json);
119+
headers.forEach((value, key) => {
120+
clone.headers.set(key, value);
121+
});
122+
123+
return json({ ...(data || {}), ...clerkState }, clone);
124+
};
125+
126+
export function injectRequestStateIntoDeferredData(
127+
data: ReturnType<typeof defer>,
128+
requestState: RequestState,
129+
context: AppLoadContext,
130+
) {
131+
const { clerkState, headers } = getResponseClerkState(requestState, context);
132+
133+
// Avoid creating a new object here to retain referential equality.
134+
data.data.clerkState = clerkState.clerkState;
135+
136+
if (typeof data.init !== 'undefined') {
137+
data.init.headers = new Headers(data.init.headers);
138+
139+
headers.forEach((value, key) => {
140+
// @ts-expect-error -- We are ensuring headers is defined above
141+
data.init.headers.set(key, value);
142+
});
143+
}
144+
145+
return data;
146+
}
147+
148+
/**
149+
* Returns the clerk state object and observability headers to be injected into a loader response.
150+
*
151+
* @internal
152+
*/
153+
export function getResponseClerkState(requestState: RequestState, context: AppLoadContext) {
154+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
155+
const { reason, message, isSignedIn, isInterstitial, ...rest } = requestState;
114156
const clerkState = wrapWithClerkState({
115157
__clerk_ssr_state: rest.toAuth(),
116158
__frontendApi: requestState.frontendApi,
@@ -126,15 +168,14 @@ export const injectRequestStateIntoResponse = async (
126168
__clerkJSUrl: getEnvVariable('CLERK_JS', context),
127169
__clerkJSVersion: getEnvVariable('CLERK_JS_VERSION', context),
128170
});
129-
// set the correct content-type header in case the user returned a `Response` directly
130-
// without setting the header, instead of using the `json()` helper
131-
clone.headers.set(constants.Headers.ContentType, constants.ContentTypes.Json);
132-
observabilityHeadersFromRequestState(requestState).forEach((value, key) => {
133-
clone.headers.set(key, value);
134-
});
135171

136-
return json({ ...(data || {}), ...clerkState }, clone);
137-
};
172+
const headers = observabilityHeadersFromRequestState(requestState);
173+
174+
return {
175+
clerkState,
176+
headers,
177+
};
178+
}
138179

139180
/**
140181
* Wraps obscured clerk internals with a readable `clerkState` key.

playground/remix-node/app/root.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import type { DataFunctionArgs, Headers } from '@remix-run/node';
1+
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
22
import type { MetaFunction } from '@remix-run/react';
3-
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
3+
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
44
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
55
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
6+
import { Suspense } from 'react';
67

78
export const loader = (args: DataFunctionArgs) => {
89
return rootAuthLoader(
910
args,
1011
({ request }) => {
1112
const { user } = request;
13+
const data: Promise<{ foo: string }> = new Promise(r => r({ foo: 'bar' }))
1214

1315
console.log('root User:', user);
1416

15-
return { user };
17+
return defer({ user, data }, { headers: { 'x-clerk': '1' } })
1618
},
1719
{ loadUser: true },
1820
);
@@ -27,10 +29,8 @@ export function headers({
2729
loaderHeaders: Headers;
2830
parentHeaders: Headers;
2931
}) {
30-
return {
31-
'x-parent-header': parentHeaders.get('x-parent-header'),
32-
...getClerkDebugHeaders(loaderHeaders),
33-
};
32+
console.log(loaderHeaders)
33+
return loaderHeaders
3434
}
3535

3636
export const meta: MetaFunction = () => {
@@ -60,6 +60,11 @@ function App() {
6060
<Links />
6161
</head>
6262
<body>
63+
<Suspense fallback="Loading...">
64+
<Await resolve={loaderData.data}>
65+
{val => (<>Hello {val.foo}</>)}
66+
</Await>
67+
</Suspense>
6368
<Outlet />
6469
<ScrollRestoration />
6570
<Scripts />

0 commit comments

Comments
 (0)