Balancing Server Loader Calls vs Client Cache Freshness in Remix Route Navigation
0 reputation · 14 Feb 2023, 06:13 UTC
0 reputation · 14 Feb 2023, 06:13 UTC
Decide which Remix routes must call serverLoader() from their clientLoader on every client‑side navigation to serve server‑authoritative data, and which routes can safely rely on cached client data without that call.
Remix’s documented loader split does not invoke the server loader automatically; invoking it adds server‑side work and may recreate the load you wish to avoid, while omitting it leaves data freshness to application policy and risks stale UI after mutations. Loader names (clientLoader, serverLoader, hydration fallbacks) and default behaviors vary across Remix/React Router versions, so the exact API to use is version‑specific. There is no built‑in default freshness policy, leaving the trade‑off between data freshness and server load as an unresolved decision.
Which routes should enforce a server call on every client navigation, and which can tolerate cached data? How can you evaluate the freshness‑vs‑load trade‑off for a given route without introducing instrumentation that alters the behavior you are measuring?
34775 reputation · 14 Feb 2023, 16:15 UTC
Enforce a serverLoader() call from clientLoader on every client-side navigation for routes that mutate server state or render user-specific data. Skip the call for static, read-only routes whose data does not change during a user session. For routes that display shared data which can change externally, use a lightweight freshness check and call serverLoader() only when that check indicates staleness.
The likely reason this feels unresolved is that Remix v2 decoupled clientLoader and serverLoader without providing a default freshness policy. On client-side navigation, only clientLoader runs; the server loader does not execute automatically. This is intentional—it prevents redundant server work—but it shifts freshness responsibility to the developer.
Confirmed behavior in Remix v2 and React Router v6.4+: clientLoader receives the same arguments as serverLoader, so you can forward the call without duplicating logic. In Remix v1, this split API does not exist, so the pattern below assumes v2 or later.
export async function clientLoader({ request, params, context }) {
// Always revalidate on the server for mutable or per-user data
return await serverLoader({ request, params, context });
}
const staticData = { /* ... */ };
export async function clientLoader() {
return staticData;
}
export async function clientLoader({ request, params, context }) {
const cached = readCache(params.id);
if (cached && !isStale(cached.etag)) return cached;
return await serverLoader({ request, params, context });
}
Keep any side effects inside serverLoader idempotent so hydration and navigation do not double-count analytics or logging.
Measure the policy rather than instrumenting the loaders themselves:
serverLoader.artillery or k6) with the policy enabled and disabled for a high-traffic route to verify latency stays acceptable.node -p "require('./package.json').dependencies['@remix-run/react'] || require('./package.json').devDependencies['@remix-run/react']"Which Remix version is your project using? If you are on v1, the split loader API is unavailable, and you must implement this decision with a single loader plus manual revalidation instead of the forwarding pattern above.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.