Remix loader private-only flag to prevent client serialization
0 reputation · 23 Dec 2021, 10:47 UTC
0 reputation · 23 Dec 2021, 10:47 UTC
When developing a Remix route, I want to guarantee that data fetched in a loader never becomes part of the client‑side hydration payload, even if the loader returns a large object that contains fields meant to stay server‑only.
The framework currently serializes every loader return value to JSON, and there is no declarative way to mark a loader as “private‑only” so that its output is stripped before transmission. This forces developers to repeat manual field‑filtering or authorization checks in each loader, increasing the chance of oversight, especially in nested routes where parent loaders may unintentionally expose data.
Is there a configuration option or convention that can globally suppress loader serialization for specific routes? Can a loader be annotated to opt‑out of client‑side hydration while still executing server‑side logic? Are there patterns to enforce this behavior across nested routes without duplicating filtering code?
34775 reputation · 23 Dec 2021, 21:17 UTC
Remix does not provide a configuration option or decorator to mark a loader as “private‑only” so that its return value is omitted from the client‑side hydration payload.
useLoaderData.request.context) in middleware and read it inside the loader without returning it.// utils/stripPrivate.ts
export function stripPrivate<T extends object>(data: T, keys: (keyof T)[]): Omit {
const copy = { ...data };
for (const k of keys) {
delete copy[k];
}
return copy as Omit;
}
// app/routes/example.tsx
import type { LoaderFunction } from '@remix-run/node';
import { stripPrivate } from '~/utils/stripPrivate';
export const loader: LoaderFunction = async ({ request }) => {
const raw = await fetchServerData(); // { secret: 'token', public: 'info' }
return stripPrivate(raw, ['secret']);
};
If you prefer not to return the data at all, attach it to request.context in middleware:
// app/middleware.ts
export async function middleware(request: Request, next: () => Promise) {
request.context = { ...request.context, secretToken: await getToken() };
return next();
}
Then read it inside the loader without returning it:
export const loader: LoaderFunction = async ({ request }) => {
const secret = (request.context as any).secretToken; // server‑only
const publicData = await fetchPublicInfo();
return { publicData };
};
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.