[RFC] SPA Mode Enhancements #3394
Pinned
tannerlinsley
started this conversation in
RFCs
Replies: 1 comment
-
Alright, so it seems this the right direction to go. We need to start outlining some things to actually build:
|
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
TanStack Start is a unique opportunity for SPAs to have server-side features, but may come with some decisions to make.
What does SPA mode mean?
From my perspective, SPA mode is a general term for NOT server-side rendering a page, which leaves only one way to get the html...
Static Files
Generating static files is the only way to get the html for an "SPA" in the traditional sense of the term. So which files are necessary?
This brings me to my first question: What is an
index.html
file?Fallback !== Index
Traditionally, we see this recommendation in a _redirects file:
This tells the server that for any paths that don't exist statically, to redirect to the index.html file.
Non-prerendered index.html
The above configuration works great for a non-prerendered index.html file. The expectation here is that your
index.html
file now a "shell" of the app with no meaningful content other than the necessary assets to client-render the app into an empty slot.Prerendered index.html
With prerendering, however, we likely have a different expectation that the
index.html
file is a fully rendered page representing the index page of the app. Now consider the following scenario:/non-prerendered-route
/index.html
The above flow produces a flash of content that is far from ideal. So what can be done?
"Shell" vs "Index"
Instead of using the
index.html
file as a "shell" AND a "prerendered" page, we can instead have two separate files:_shell.html
- a true shell of the app that can be used to render any route*.html
- Prerendered pages that can be used to render specific routes, including theindex.html
fileThis way, we can have the best of both worlds. Any prerendered pages can be served as static files, and the
_shell.html
file can be used as a proper fallback for any routes that don't have a prerendered page.Implementation
If we now need to generate a "shell" with no meaningful content as a fallback, we need to go a level higher in the rendering process. If we break down our existing layout strategies, we find that there is already a great place to do this: The
__root.tsx
file. The root component represents the root of the app and is always rendered regardless of the page/route.In our
__root.tsx
file, we effectively start rendering page-specific content where the first<Outlet />
is rendered. By "removing" the<Outlet />
from the__root.tsx
file, we can effectively render a "shell" of the app that can be used as a fallback for any routes that don't have a prerendered page.How do we "remove" the
<Outlet />
?Let's first assume that we know when the router is currently rendering the "shell" of the app via a new
router.isShell
flag. Obviously this isn't a real path or route, so we will likely use the index (/
) pathname as a runtime placeholder just in case anything in the shell rendering logic attempts to read from the current location.While we could push the responsibility of "removing" the
<Outlet />
to the user, I think it makes more sense to have this behavior require zero changes to the user's code. My best idea so far is to update the<Outlet />
component logic to something like this:React's secret "client-only mode
https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content
In React, if we throw an error during SSR that's inside of a suspense boundary:
createRoot().render()
, but only for that tree.We can use this approach to solve 2 problems:
pendingComponent
to your root route!ssr: false
on it, we can error it on the server, then pick up on the client and rerender from thereRendering the Shell
Now that we have a means to "remove" the
<Outlet />
from the__root.tsx
file, we need to render the "shell" of the app. This will mostly likely involve checking the app's configuration to see if "SPA Mode" is enabled, and if so, rendering the "shell" of the app.As of right now, we would need to either extend Vinxi with additional logic to do a special prerender OR do the migration off of Vinxi to Nitro so we have more control.
However, I have always been a fan of calling things what they are and I think this is a case that could benefit by removing the "SPA Mode" terminology. Instead, I think it makes more sense to call this functionality by its outcome,
shell: true
or even `shell: '_shell.html' if you want to specify a custom file name.This way when we configure our app in
app.config.tsx
we not only have a clear way of enabling an "SPA Mode" but we also have a clear way of configuring the output of the "shell" of the app:The outcome is predictable from the name alone. This setting, when enabled, will only render the "shell" of our app to the
_shell.html
file in our public output directory.Note
Why not just create a route for the shell and not render anything in it?
There are a number of reasons why I dont' think this is a good idea:
Shells and Robots
It's possible that with a
_shell.html
file now output in our public directory, that a robot may stumble upon it and index it. This could be problematic for SEO, so there are a few things we can do to mitigate this:<meta name="robots" content="noindex, nofollow">
tag.robots.txt
file as an exclusion.hydrateRoot
vscreateRoot
Something we need to consider is the difference between
hydrateRoot
andcreateRoot
from React. Typically we only want to createRoot() and render() when there is no content to hydrate at all. After consulting with Josh Story from the React team, I've learned that callinghydrateRoot
, even in circumstances where only a partial match is being hydrated will still result in the most performant outcome.Sticking with this recommendation, our
client.tsx
entry files can avoid breaking changes to existing code.SPA Mode = No SSR + Server Functions + API Routes
All of the above makes the assumption that there is no
ssr
at all, but that shouldn't mean "no server" completely. The whole idea of an SPA mode is made most valuable when you also have the ability to:To do this, we effectively need a way to disable the SSR "router" or handler from handling routes at all. We'll need to figure this out. See below for more info
Hybrid approach?
Does it make sense at all to somehow be able to have an SSR server as well? Anything SSR related could be optional and in addition to the above except for the fallback redirect.
As soon as you have any SSR handler, your http stack needs to go from something like this:
to
To give the SSR handler a chance, we can no longer use our fallback redirect to _shell and instead fall back to SSR being a requirement.
Some SSR or none at all
To me it seems like you need to make the choice up front whether you are going to do some SSR or none at all, which directly affects the decision up front to either:
SSR + SPA Pages
If the user does want to do some SSR, and the SSR handler exists, that doesn't mean every page should have to be server side rendered. Technically, only the shell would need to be server rendered to achieve the same experience as a _shell + fallback approach.
To do this, we would like need to update our existing SSR handler to detect routes with
ssr: false
. But a few questions:<Outlet />
rendersnull
on the server?Beta Was this translation helpful? Give feedback.
All reactions