Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to ssr:false + prerender scenarios #12948

Merged
merged 21 commits into from
Feb 11, 2025

Conversation

brophdawg11
Copy link
Contributor

@brophdawg11 brophdawg11 commented Feb 4, 2025

@brookslybrand and I did a deep dive on some of the current nuances/pain points with ssr:false and prerender scenarios. In Remix v2, we only had the ssr config so ssr:false alone implied "SPA Mode". When we added prerender in RR v7, we didn't quite nail all of the changes to ensure that differences between ssr:false versus true "SPA Mode" were correctly updated.

  • "SPA Mode" means a single HTML page capable of hydrating for any route. It can do this because it only renders the root route HydrateFallback on the server and then loads the proper client-side matched routes via route.lazy during hydration. This is what you get with ssr:false and no prerender config.
  • In Remix v2 and so far in RRv7, we didn't allow any loaders when in SPA mode. This PR updates that logic so that you can have a root loader because it does always run on the server and makes loaderData available to HydrateFallback as an optional prop so you can render a more full-featured loading page.
  • Once you add a prerender config, you're opting into pre-rendering non-SPA (effectively MPA) pages for those routes - and those will be able to run the loaders below the root and prerender complete HTML docs in SSG fashion. Those HTML docs still hydrate into client-side-navigable applications.
  • That left a small gap in real world use cases where once you added ssr:false + prerender:['/'], you lost your ability to serve SPA mode any longer because the main index.html file was now an MPA that included routes/_index. In these scenarios, prerender will now output a special __spa-fallback__.html file that you can confiugure your static server to serve for any paths you didn't prerender. This is powerful because you can still use ssr:false, fully prerender some paths for perf/SEO, and still allow users to enter your app on any other path via the SPA fallback.

A few other small updates:

  • We now also throw errors for action/header exports when using ssr:false
  • Our dev server will now 404 on non-prerendered routes when using ssr:false and a prerender config to better align with production behavior

TODO:

  • Docs

Sorry, something went wrong.

…render combinations
Copy link

changeset-bot bot commented Feb 4, 2025

🦋 Changeset detected

Latest commit: 9c8a9bf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
react-router Major
@react-router/dev Major
@react-router/architect Major
@react-router/cloudflare Major
react-router-dom Major
@react-router/express Major
@react-router/node Major
@react-router/serve Major
@react-router/fs-routes Major
@react-router/remix-routes-option-adapter Major
create-react-router Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@timdorr timdorr changed the title Improvements to ssr:false + prerender sceanrios Improvements to ssr:false + prerender scenarios Feb 4, 2025
@brophdawg11 brophdawg11 force-pushed the brophdawg11/spa-prerender branch from 43853ec to 9778c3d Compare February 6, 2025 19:59
@brophdawg11 brophdawg11 force-pushed the brophdawg11/spa-prerender branch from 0df6d0c to b9cf540 Compare February 6, 2025 20:37
Comment on lines +1684 to +1686
if (route.id === "root" && exp === "loader") {
return false;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Allow the root route to have a loader in SPA mode

Comment on lines -2088 to -2172
(Array.isArray(reactRouterConfig.prerender) &&
reactRouterConfig.prerender.length === 1 &&
reactRouterConfig.prerender[0] === "/"))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This went a bit too far - ssr:false, prerender:['/'] is an explicit opt-into prerendering the / route and should trigger full SSG of the / route and prerender past the root.

let request = new Request(`http://localhost${reactRouterConfig.basename}`, {
headers: {
// Enable SPA mode in the server runtime and only render down to the root
"X-React-Router-SPA-Mode": "yes",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use this during build-time request handling now so we can render a SPA fallback even when build.isSpaMode is false because we're prerendering some MPA pages

@@ -21,7 +21,11 @@ export interface ServerBuild {
assetsBuildDirectory: string;
future: FutureConfig;
ssr: boolean;
/**
* @deprecated This is now done via a custom header during prerendering
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No longer used by our server - but since ServerBuild is arguably public API we can just deprecate it

Comment on lines -2124 to -2207
validatePrerenderedResponse(response, html, "SPA Mode", "/");
validatePrerenderedHtml(html, "SPA Mode");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inlined these assertions for clarity

// they can serve. Otherwise it can be the main entry point.
let isPrerenderSpaFallback = build.prerender.includes("/");
let filename = isPrerenderSpaFallback
? "__spa-fallback__.html"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Write out this spa fallback file when the user chose ssr:false + prerender:['/'] as a way for them to load/hydrate into non-prerendered paths without a runtime server

Comment on lines -2151 to -2248
let routesToPrerender: string[];
if (typeof reactRouterConfig.prerender === "boolean") {
invariant(reactRouterConfig.prerender, "Expected prerender:true");
routesToPrerender = determineStaticPrerenderRoutes(
routes,
viteConfig,
true
);
} else if (typeof reactRouterConfig.prerender === "function") {
routesToPrerender = await reactRouterConfig.prerender({
getStaticPaths: () =>
determineStaticPrerenderRoutes(routes, viteConfig, false),
});
} else {
routesToPrerender = reactRouterConfig.prerender || ["/"];
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was lifted and is now available on build.prerender

Comment on lines -2419 to -2503
loader: route.module.loader ? () => null : undefined,
action: undefined,
handle: route.module.handle,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

None of this is needed on the routes, we just need to know id/index/path/children to make our prerender decisions

@@ -347,7 +361,6 @@ export function createClientRoutes(
"No `routeModule` available for critical-route loader"
);
if (!routeModule.clientLoader) {
if (isSpaMode) return null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are unneccesary because fetchServerLoader already returns null if no loader exists

if (
!_build.ssr &&
_build.prerender.length > 0 &&
normalizedPath !== "/" &&
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to self - ensure we have correct behavior for hard reloads on / in dev mode when it's not prerendered

This reverts commit 8fafeea.

No longer needed now that we do export detecton during the virtual route manifest module creation - since that gets invalidated on config change
@brophdawg11 brophdawg11 force-pushed the brophdawg11/spa-prerender branch from a2f3576 to 9c8a9bf Compare February 11, 2025 19:56
@brophdawg11 brophdawg11 merged commit abe8008 into dev Feb 11, 2025
8 checks passed
@brophdawg11 brophdawg11 deleted the brophdawg11/spa-prerender branch February 11, 2025 20:56
Copy link
Contributor

🤖 Hello there,

We just published version 7.2.0-pre.0 which includes this pull request. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

@brophdawg11 brophdawg11 linked an issue Feb 12, 2025 that may be closed by this pull request
wilcoxmd added a commit to wilcoxmd/react-router that referenced this pull request Feb 13, 2025
…d-route-typegen

* upstream/dev:
  bump patch to minor for new API: `href`
  Type-safe href (remix-run#12994)
  docs: prerender/ssr:false (remix-run#13005)
  Skip action-only resource routes with prerender:true (remix-run#13004)
  Update docs for spa/prerendering
  Improvements to ssr:false + prerender scenarios (remix-run#12948)
Copy link
Contributor

🤖 Hello there,

We just published version 7.2.0 which includes this pull request. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

namoscato added a commit to namoscato/tiny-truths that referenced this pull request Feb 27, 2025
per remix-run/react-router#12948 improvements
Copy link
Contributor

github-actions bot commented Mar 6, 2025

🤖 Hello there,

We just published version 7.3.0 which includes this pull request. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SPA Mode should not use Single Fetch revalidation behavior
2 participants