👋 Hello Redwood community, this is your chance to shape one of the major feature…s in upcoming versions of Redwood! Let's see if we can build this feature together!
> How you can help!
> 1. Be Kind! This isn’t about Redwood vs X - we love other frameworks in the JS community - they all bring something unique, and we hope that Redwood adds to it. There’s no need to choose sides!
> 2. Read through the RFC - do the APIs and concepts it introduces feel intuitive to you?
> 3. Tell us about how you would use it on your project - go into detail about why you can’t achieve this without render modes - and how you see this improving your product/workflow/dx. Tell us about what you are building and your existing workarounds!
> 4. Do you use something similar in another framework that you think is worth us exploring?
> 5. Are we missing any features you were expecting?
> 6. Look through part IV - there’s some fundamental questions here about the concepts - we would love your views!
### I. Summary
Introduce the concept of “render modes” - a prop in the Routes file to determine how a Route (i.e. page) will be rendered. Render modes change two things:
1. **Where** the route is rendered - server or client
2. **How** the route is rendered - as HTML-only, a hydrated JS page, a hydrated JS page with dynamic HTML `<head>` tags
```jsx
// web/src/Routes.tsx
<Routes> {/*👇 set mode */}
<Route name="home" component={HomePage} renderMode="static"/>
<Route name="dashboard" component={Dash} renderMode="server"/>
</Routes>
```
Proposed render modes are:
- `static` - render this page as pure HTML with no JS tags, on every request to the server
- `server` - render this page on every request on the server. This is equivalent to SSR.
- `client` - traditional JAM Stack rendering - i.e. return a blank page with JS tags, and let the client do the rendering [this is the only currently supported mode].
- `meta` - only render up-to and including the MetaTags in a page component. This seems to be a common reason why users want SSR, to generate dynamic OpenGraph headers. “Partial SSR” like this keeps complexity low in your codebase, but still gives you the ability to generate dynamic `<meta>` tags for link previews on socials/slack/discord
- Future: `stream` - this depends on React 18, and there’s a lot more to understand here before we make it official
The reason we offer render modes, is so that you can choose the level of complexity you want in your application. Everything in software is a tradeoff!
The more you render on the server, the higher the complexity is - you need to make sure your page (and all of its downstream components) can be rendered server-side correctly - just like prerendering.
Somewhat ironically too - although server-rendering can improve performance in most cases, as your app gets popular unless your infrastructure can keep up with the number of requests, it may actually *degrade* performance.
### II. **************************************Other new concepts:**************************************
### **Route hooks & Server data**
In Redwood V3 we introduced the concept of route hooks to provide `routeParameters` when you are prerendering a route with a dynamic route parameter. Prerender runs at build time, so before a route is rendered, the build process will call the routeParameters function, before trying to render the route.
When doing server or meta rendering - a similar process happens, but this time during runtime. i.e. During the request → hook → render → response cycle. You may choose to run some additional code on your server, *before* rendering your route. This is analogous to `getServerSideProps` in NextJS and `dataLoaders` in Remix - where you can run a function to define some values, eventually supplied as props to your component. In Redwood though, we’re proposing receiving these values with a hook.
![Untitled-2022-10-28-1625](https://user-images.githubusercontent.com/1521877/198556137-7dd502b4-035e-42b3-8675-8418f9885e88.png)
This has two parts to it:
1. `serverData` - a function defined in your `page.routeHooks.ts` file that will return whatever you want to provide to your page. This function runs right before your route is rendering but ***************on the server*************.** This means you have direct access to your DB, or can do fun stuff like dynamic OG image generation. But careful…. you could just break rendering if your serverData hook is computationally too expensive!
2. `useServerData` - a react hook, that lets you consume the data in your components. Remember that server data is provided at *****page***** or *****route***** level, so if you use the `useServerData` in a nested component, it’ll just load your route’s data.
There are some edge-cases here that need fleshing out - and is covered in section IV-V
### **Preload directives & Injecting the correct chunk**
I’ve done a bit of research into how best to inject the correct bundle on first render with Vite, and to preload chunks where required. The rough idea around this is:
**A. at build time, create a map of route to relevant code-split chunk** e.g.
```jsx
{
'/dashboard/teams': 'Teams.aslknasdg.js',
'/dashboard/jobs': 'Jobs.alkn135.js'
}
```
******B.****** When rendering on the server, ensure that the correct bundle is injected (alongside any main/base bundle)
**C.** If the page contains links to other pages, we need to hold on to this list (new feature on RWRouter), and add preload directives onto the page for each of them
**D.** On hover of a link, we can also load the relevant bundle
---
## III. Prerequisites
- Vite integration - not strictly required, but our preferred route
- A frontend server - either express or fastify
- Configuration on “JAMStack” providers to route requests to the frontend server
- Configuration on traditional providers to route requests to the frontend server (i.e. do not deploy static sites)
---
## IV. Outstanding Conceptual Questions aka Help wanted!
1. **Where do you set cache control headers?**
Any render modes involving the server benefit from setting cache control headers (and allow doing things like stale-while-revalidate). Options:
**a) Define it with additional props on the Route**
```jsx
// web/src/Routes.tsx
<Routes>
<Route
renderMode="static"
name="home"
component={HomePage}
// either as object or string
cacheHeaders="Cache-Control: max-age=1, stale-while-revalidate=59"
/>
<Route
...
/>
<Route
...
/>
```
**b) In the routesHook file**
```jsx
// web/src/pages/HomePage/HomePage.routeHooks.ts
// getCacheHeaders essentially - called after rendering the page
export const cacheHeaders = (req, html) => {
return {
'Cache-Control': 'max-age=1, stale-while-revalidate=59',
}
}
```
In both cases, we could abstract the specific header names - but not sure if there’s much value in abstracting them.
2. **What is the benefit of prerendering server-rendered pages?**
I’m not sure if there’s any advantage to this (apart from the first request maybe). Curious if the community has any thoughts/usecases that might make this worth it? Otherwise we will just disable prerendering, unless your renderMode is `client`
It may be an anti-pattern to serve prerendered files via a server too - especially in serverless where we would access the filesystem for each request. Serverful would allow us to load all the content once.
3. **Suggestions for the names of the render modes**
Do they make sense to you? Does it sufficiently describe the choice you are making? Some other terms I’ve considered:
- hydration rendering or csr (instead of client)
- ssr (instead of server)
- HEAD rendering (instead of meta)
4. **Automatically Composing serverData hooks**
I’m not sure if this is even desired. But let’s say you have `/dashboard/profile` and `/dashboard/team` . Each page can have a different routeHook - but what if I want to share the same hook? You can always call another serverData hook in your currnet Hook
This would require the router to support nested routing - which has certain pitfalls we've intentionally avoided for a while.
6. **Capacity based rendering**
A huge downside of server-rendering that often gets overlooked is “what happens if my rendering server starts choking?”. This can happen for any number of reasons - e.g. maybe the database you’re accessing during the render has run out of connections. Or maybe, one of the pages has a memory leak and crashes - does this mean ****all**** your users should suffer?
I’d like to explore how we can fallback to client side rendering - we can leverage fastify plugins for this potentially, when our server is running out of capacity. Importantly - what does this mean in terms of how you build your app? If you are expecting to do SSR, maybe you are using `routeHooks` to provide some data to your page, what happens to this data if we fallback to client rendering?
7. **Auth & Server-side rendering**
Currently all supported Redwood auth providers except dbAuth stores auth tokens in the browser storage which is only available client side. We may need to transition to using cookies to be able to render authenticated pages on the server. Concepts to consider:
- Performance impact of reading a cookie on every request
- The auth cookie will be validated by code living in the root/app-level `serverData` routeHook.
~- How to maintain the DelightfullySimple™ workflow of using Redwood auth - we will need to get auth tokens from providers and save details in a cookie. How do we achieve this? With a redirect (Set-Cookie on the server) or just using JS? Using “static” render-mode might make things complicated here!~
~- How do you regularly update the cookie? A redirect feels very heavy handed!~
- The conceptual complexity when you start caching your pages - how do you make sure you cache pages per user? These are typically handled at the infrastructure layer (are VARY headers still a thing?) - but it should feel really simple. Perhaps we need to talk to the Remix community for some ideas!
8. **Deploying to CloudFlare/V8 Isolates**
My knowledge on this is very shallow. I’d like to understand a bit more about what it would take to run a Fastify server with React running on Vercel’s middleware, or Cloudflare’s workers.
Importantly, from a Redwood user’s perspective it should all be transparent and JustWork™. One of our core tenants has been to not distract the user from doing what they want to do - build their app - by introducing concerns/complexity around infrastructure until they are ready to deal with it.
9. **Where do we draw the line between keeping logic in GraphQL vs serverData hooks?**
~I’m not clear on this - and I worry that we may make it confusing for users.~
See update on this below https://github.com/redwoodjs/redwood/issues/6760#issuecomment-1347989389
## V. Other Practical Things
1. **The code for loading chunks during navigation has to be refactored in the router** - this is a long standing problem, even with prerendering.
Currently, even when the page has been rendered by the server, it gets ************re-rendered************ on the client (presumably because JS on the client side will set a blank loader while it loads the chunk)
1. **We need to refactor the Router to load pages under Suspense.** This will simplify the server-side rendering process significantly (as we’ll not need to introduce additional babel code and vite plugins to handle `require` statements).
1. **We need to measure the impact of server side rendering**
Not super clear to me how we do this yet, perhaps we use a tool like k6. Things we would need to check in particular:
- Nested Cells (i.e. waterfalls) - and how this impacts performance / scalability. Render modes can remove waterfalls completely - because all the cells in the render path can run their queries on the server…. but we need to measure at what cost.
- Serving `static` routes from a frontend server with cache headers vs serving the same file from CDN.
- Running things like playwright in the serverData hook, or an external service to give you a custom OG image
2. **Handle `<Redirects/>`**
It’s not currently possible with rwjs/router to Redirect a user with the correct HTTP status code with the Router. What we’ll need to do is gather all the Redirects in a render path, and at the end send a 301/302 with the final path.
1. **Under the current proposal the FE server is separate from the Graphql/Backend server**
This has certain advantages e.g. if you change your SDL for your GraphQL your frontend does not need to restart - this is a common problem in the Next/Remix world (I think!). The architecture is more resilient as well - if your frontend server crashes, there’s no reason your mobile apps shouldn’t continue working with the API.
But it does bring more complexity - we are deploying two separate apps in essence - deployments can go out of sync and we are blurring the lines between FE and BE. If we use workspaces (like we currently do) - we may need to think about how to communicate that features like service caching are independent and **may** not be available to the frontend server.
### Are you interested in working on this?
- [X] I'm interested in working on this