Skip to content

fix: execute loaders during SSR when runLoaders is true#110

Merged
uhyo merged 2 commits intomasterfrom
claude/fix-ssr-loader-hydration-r9bXA
Feb 20, 2026
Merged

fix: execute loaders during SSR when runLoaders is true#110
uhyo merged 2 commits intomasterfrom
claude/fix-ssr-loader-hydration-r9bXA

Conversation

@uhyo
Copy link
Owner

@uhyo uhyo commented Feb 19, 2026

Previously, setting runLoaders: true in the SSR config only caused
routes with loaders to be matched, but data was always undefined.
Now loaders are actually executed with a synthetic request constructed
from ssr.path, and the results are passed to components as the
data prop during server rendering.

https://claude.ai/code/session_01MnPMAHDZcyhs5cgBR5Xtae

Previously, setting `runLoaders: true` in the SSR config only caused
routes with loaders to be matched, but data was always `undefined`.
Now loaders are actually executed with a synthetic request constructed
from `ssr.path`, and the results are passed to components as the
`data` prop during server rendering.

https://claude.ai/code/session_01MnPMAHDZcyhs5cgBR5Xtae
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates SSR behavior so that when ssr.runLoaders: true is provided, matched route loaders are executed and their results are provided to route components via the data prop during the SSR/hydration matching path.

Changes:

  • Execute matched route loaders during the SSR/hydration route-matching branch when runLoaders is enabled.
  • Update SSR tests to assert loaders are invoked and data is rendered.
  • Update docs to reflect that loaders execute during SSR when runLoaders is true.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
packages/router/src/Router.tsx Runs loaders during the SSR/hydration matching path and threads results into matched route data.
packages/router/src/tests/fallback.test.tsx Adjusts SSR test expectations to verify loader execution and rendered loader output.
packages/docs/src/pages/LearnSsgPage.tsx Updates docs copy to reflect SSR loader execution behavior when runLoaders is enabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 214 to 218
return matched.map((match) => {
const data = match.route.loader
? match.route.loader({
params: match.params,
request,
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

ssr.runLoaders executes loaders by calling match.route.loader(...) directly. Unlike the client path (which uses executeLoaders + caching), this can re-run loaders on re-renders (e.g., parent re-render with a new ssr object, or React StrictMode remounts), causing duplicate network requests/side effects. Consider adding a memoization/cache for SSR loader results keyed by ssr.path (or otherwise making this execution idempotent across re-renders), and/or ensure the memo dependencies are based on ssr.path/ssr.runLoaders rather than the ssr object identity.

Copilot uses AI. Check for mistakes.
Comment on lines 213 to 214
const controller = new AbortController();
return matched.map((match) => {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The AbortController created for SSR loaders is never aborted. If runLoaders is used during hydration (where locationEntry can transition from null to non-null) or if the Router unmounts while loader promises are still pending, those requests can't be cancelled and may continue doing work unnecessarily. Consider storing the controller in a ref and aborting it in an effect cleanup / when switching away from the SSR branch.

Copilot uses AI. Check for mistakes.
Comment on lines 210 to 211
// Execute loaders during SSR with a synthetic request
const url = new URL(ssr.path, "http://localhost");
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The synthetic URL for SSR loaders is always created with a hard-coded origin (http://localhost). Loaders that read new URL(request.url) may behave differently in SSR than in production (host/protocol/cookie scoping, absolute URL generation, etc.). Consider allowing SSRConfig to accept a full URL/origin (or even a Request) so the server can provide the real origin/headers when executing loaders.

Suggested change
// Execute loaders during SSR with a synthetic request
const url = new URL(ssr.path, "http://localhost");
// Execute loaders during SSR with a synthetic request.
// Prefer a full URL if ssr.path is absolute; otherwise, use an origin
// provided by SSR config (if any), falling back to localhost.
let url: URL;
try {
url = new URL(ssr.path);
} catch {
const ssrAny = ssr as any;
const baseOrigin =
typeof ssrAny?.origin === "string" && ssrAny.origin.length > 0
? ssrAny.origin
: "http://localhost";
url = new URL(ssr.path, baseOrigin);
}

Copilot uses AI. Check for mistakes.
Merge the SSR-with-loaders and client-side code paths into a single
unified path that calls executeLoaders. This avoids duplicating loader
invocation logic and makes the hydration transition from SSR (locationEntry
null) to client (real entry) flow through the same executeLoaders function
with consistent caching.

https://claude.ai/code/session_01MnPMAHDZcyhs5cgBR5Xtae
@uhyo uhyo merged commit a3b0125 into master Feb 20, 2026
1 check passed
@uhyo uhyo deleted the claude/fix-ssr-loader-hydration-r9bXA branch February 20, 2026 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants