RFC: Static Pages / Browser Only Fetching #2853
Replies: 20 comments 11 replies
-
About prerendering, do you think it would be possible to support both SSR and prerendering at the same time? I'm thinking on something like preprender the most popular pages of a website (e.g. popular products, latest articles, etc.) and keep SSR for the rest, another thing is that I could store the date (or Remix could do it automatically when the prerender happen) and in the loader verify "was this page prerendered in the last week? then continue using the same data/html, is it older? then do SSR" this way you can use preprendering mostly for a few pages, this will only work if prerendering is "call the loader at build time and SSR at runtime". Another feature that could be interesting is if a route doesn't need any loader (like an about page) then prerender it automatically. If the root has a loader then this will not happen thought and it's super common to have one there so maybe doesn't deserver the effort to implement this. Also because of the nested layouts this would mean detect if not only the leaf route but any other parent route doesn't have a loader. About client loader, what would happen when the user opens a new browser tab and goes to a page with a client loader, what will the user see while the loader is running client-side? Right now, the user browser will take care of showing a spinner, but with the loader working client-side Remix would need to send an empty or almost empty page, download the component and client-loader code and then call the loader, once finish render the page. Should there be a way to export a fallback page to render server-side in those cases? export function Fallback() {
return <h1>Loading...</h1>
} Something like that, it could also be called Skeleton, but fallback makes more sense in React since Suspense uses that word. With this Remix can SSR the fallback/skeleton of that route (inside any parent layout if needed) and then after calling the client-loader switch to the actual page. Another option could be to provide a way for the page to detect if it is still pending to be rendered. export default function View() {
let isPending = useIsPendingClientLoaderOnFirstCall() // name TBD
if (isPending) return <h1>Loading...</h1>
return <ActualUI />
} |
Beta Was this translation helpful? Give feedback.
-
Great ideas and questions @sergiodxa, I just pulled up some old notes and created a new issue about |
Beta Was this translation helpful? Give feedback.
-
Coming back to this, I need to think this through some more, but I think maybe we The hook would work, but one thing I really hate in React is having to add loading/error/success branching in every component. And then when you need that data for other hooks you have to split the component up into separate components anyway. I like that in Remix your default component can always plan on having data. |
Beta Was this translation helpful? Give feedback.
-
The Pending export is way better, I also hate the branching inside components, that’s something Suspense improved a lot by moving the fallback and error handling to the parent. Next.js uses the hook approach for ISR support so you do let { isFallback } = useRouter() To know if you have to render a fallback for a new page or not and I hated using it. Remix should be better at this and the Pending export is the way. |
Beta Was this translation helpful? Give feedback.
-
For what it's worth, if you decide to go with either/or I definitely prefer the |
Beta Was this translation helpful? Give feedback.
-
Re: pre-renderingThis all sounds great. I really like the idea of providing the context object up front when pre-rendering. There's a ton of flexibility there. My main concern would be what we'd need to do in our starter templates to make sure that all of our supported hosts know how to serve the HTML pages we put in the An interesting experiment would be to do an entire site like this and deploy it to an S3 bucket and see what routing rules we need to setup on the bucket to get the site to be served correctly. Idea: exports.preRender = string | string[] | fnMaybe instead of a function, exports.preRender = ["/about", "/team"]; You wouldn't get to provide Idea: pre-rendering with path "globs"
exports.preRender = ["/about", "/blog/*"];
// or, to pre-render the whole site:
exports.preRender = "*"; This would ofc be an error if you had any route paths with dynamic segments in the matched set of routes since we can't just guess them. But it could be nice if you just had a bunch of static routes in a folder somewhere. Future idea: Add S3 as option in
|
Beta Was this translation helpful? Give feedback.
-
I love this API and the flexibility it'll bring to Remix. This looks clunky however:
I'd much prefer the |
Beta Was this translation helpful? Give feedback.
-
If you add the Build-Time Pre-Rendering feature, please allow using runtime and build-time data fetching in the same route. Build-time: Get static data of the route, like translations, an article data, etc. If you have both it will not really pre-render the HTML at build, just fetch the data and store it in the context so the loader can use it (or not if it's too old) |
Beta Was this translation helpful? Give feedback.
-
I started a discussion thread not realizing this issue was already open: I think it would be most beneficial to have a I think what I wrote and what is being proposed here is somewhat different. I think there is tremendous value in executing a loader/action on a document request (on the server) and then executing that same loader/action on the client for a transition. The client has to make an HTTP request anyways why not bypass the proxy? |
Beta Was this translation helpful? Give feedback.
-
How would you use a decorator there if they are only usable with classes and their methods? Also they are not standard yet. Also there’s a huge benefit of running loaders or actions only server side, you can recover extra data, in your example the list of gists only use two properties but each gist from the GitHub API comes with a lot, you could remove them on the server and send less data to the browser, you lose this if the loader runs client side |
Beta Was this translation helpful? Give feedback.
-
Ugh. that is weird. ok well maybe a
Sure, but you're making a trade-off between amount of data being transferred and cacheability of that data. GitHub can cache that resource on an edge server indefinitely and purge it whenever it changes. If I am forced to proxy the request, that cache is effectively useless since the user is forced to make a request to my origin server (or I run the entire app at the edge, or I handle the caching and purging myself, which may be impossible). Regardless, I don't think you can definitively say that less data transfer === less latency. Indeed, it may be the exact opposite depending on how far (geographically) the request has to travel in order to get a response. I think it would be more prudent to let engineers make that assessment on a per-loader/action basis. I find it rather odd that if you build a single-page application with React, everything from a 3rd party API is fetched on the client, but if you use a framework that enables SSR (Remix, Next.js, Gatsby, etc.) then all of the sudden all 3rd party API requests are forced to be made on the server (if you want SSR as well). |
Beta Was this translation helpful? Give feedback.
-
@mjackson I commented here with our use case before finding this discussion:
edit: the preview environment is currently using a Netlify function rather than Edge function which I guess explains it. |
Beta Was this translation helpful? Give feedback.
-
A "Browser-Only Fetching" option also seems like it could ease the migration path from React Router 6.4.0 -> Remix, since RR 6.4.0 loaders may be doing browser-only things |
Beta Was this translation helpful? Give feedback.
-
I'm working on a use case where |
Beta Was this translation helpful? Give feedback.
-
I am working on a greenfield project in the first phase, creating all the scaffolding, router, authentication, etc... It is a client-only project as requisite, everything is behind a login and using a public API. I started with CRA and React Router 6.4. After a bit of config, everything tastes a lot like Remix :). I am planning now to move it to Vite and maybe use the filesystem routing, more Remix flavour. I feel like creating a mini-Remix ;) IMHO. Having this clientLoader option will be a super boost for Remix adoption. Explaining to my boss and colleagues the need for a backend for Remix always brings a lot of friction to the table. it will be removed with clientLoader, we can just start with it and when we first need of using a backend comes: to send an email, or a cron, expose an API endpoint, manage any payment, etc... Boom! Everything wpuld be ready! |
Beta Was this translation helpful? Give feedback.
-
This related proposal for a |
Beta Was this translation helpful? Give feedback.
-
Maybe this variant will work better? export const handle = { loader: "shared" }; Instead of renaming loader and pussing some magical property to function we can use information from |
Beta Was this translation helpful? Give feedback.
-
👍 ➕ I like the concept of having a It also may open opportunities to other tangential features, like offline first loaders?
Maybe I know remix-pwa is doing a lot of heavy lifting for the user with
|
Beta Was this translation helpful? Give feedback.
-
Cross-posting a discussion from Astro, which is not exactly the same but relates to build-time prerendering: withastro/roadmap#869 (comment) To sum up the part that is relevant here: prerendering based on the dynamic route, so the request URL only, is the current way of configuring prerendering. But it doesn't have to, and in my opinion this is highly limited:
Doing so however imply that you also point users to the right page, and that's the tricky part. Hence the prerendering based on dynamic routes: web server already know how to point a URL to a file, so it's easy to prerender based on the URL and have the server to match a request to a render. But I would advocate for a bolder strategy. Here is an architecture diagram for this: Also a moonshot but I wish we could go slightly further than prerendering, but also build chunks specific to each prerendered page or alter their content. Related to an issue with i18n tokens: even with RSC, because text is always the lowest point of a React tree, text content is most often part of a client component, so the translation tokens have to hydrated and be part of the JS bundle etc. I feel like it could be interesting to allow bypassing React altogether for these, by somehow injecting the translated value directly into the code. So instead of being a prop or part of the context or whatever your translated text would be "just there". Something like macros basically but ran for each prerendered page. Sorry I am being a bit vague here but I think this area could be improved. |
Beta Was this translation helpful? Give feedback.
-
Given the recent activity... should we assume this RFC is going to be handled by RSC? Where can we track progress on that? |
Beta Was this translation helpful? Give feedback.
-
Remix is pretty opinionated about always server rendering, but that's not really an opinion, it's just a starting point.
Remix's data loading, data mutation, and asset loading conventions around nested routes are useful whether you're server rendering, fetching data clientside only, or static rendering.
The following APIs will enable you to easily change which mode your route works with:
Build-Time Pre-Rendering
It seems the thing to do in this space is make up new acronyms. So we'll call this BTPR 😋. Just kidding, we'll just call it pre-rendering.
Inside of your remix config, you can define a
prerender
The loaders for these routes will be called:
This gives you the ability to fetch everything in
prerender
or let the loaders handle it:Or put it on context in
prerender
And now a loader can just find it's post from context
When Remix is bundling, it can render these pages and dump them into your
public/
folder.Clientside Data Fetching/Mutations
Remix calls your loaders serverside, even on script transitions. This is great for a few reasons:
That said, many databases allow you to connect to them in the browser, and some pages you don't care about SSR. Perhaps you're using clientside authentication with JWTs on pages you don't care to server render and you'd like to talk directly to your APIs from the browser w/o the Remix server in the middle (like FaunaDB, Hasura, or Firestore).
If you export a
clientLoader
, then Remix will use it instead of theloader
on script transitions.There is no
context
because the server isn't involved, but we can still provide a request that is nearly identical to what theloader
would get (just no headers, pretty much). Note that in place of cookies and sessions apps we use the browser's built-insessionStorage
.And of course you can have a
clientAction
as well.One image in our mind we keep when building Remix is the idea of "levers" you can pull to change how your app is delivered w/o having to change much (or anything) about your application code. For example, removing
<Scripts/>
and your app continues to function the same except with full page document loads.A lever here could be a
sessionStorage
abstraction with the same API as our HTTP session storage. Consider the cookie session storage:We could have the same API for browser
sessionStorage
This means an app could theoretically write the code like this:
And then switch it to this:
Of course, the
request
won't have a "Cookie" header, but the browser storage can just ignore it (and the app code should probably delete that part) but the point is this is another lever you can pull to change where your code is running without having to really change your code at all.All the other tools in Remix will work identically:
usePendingLocation
,usePendingFormSubmit
, the data diffing for loaders, etc.What about both?
There's the possibility that you could export both
loader
andclientLoader
. What do we do? Useloader
for document requests andclientLoader
for script requests?I think this opens up far too many questions and is hard to pin down a solid use-case. Considering an authenticated user to get the same result on the server and the client would require a lot of code to have them authenticated in both places. Or even just a database connection, you'd need to establish it on the client and the server.
This is much easier to think about (and no doubt implement) if it's either/or.
Alternative API
Instead of changing the name of the exported function we could add a property to the function:
Now it really is just a lever to change where the code runs, it also makes it impossible to define a
loader
andclientLoader
.What do you think?
Beta Was this translation helpful? Give feedback.
All reactions