-
Notifications
You must be signed in to change notification settings - Fork 993
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
[RFC]: Render Modes - your input needed! #6760
Comments
Is there a proposal for a default rendering mode? Or would this be a required option for every route? |
First of all huge kudos for that write up - it was clear and thorough! FWIW I haven't used Redwood yet in production at all, played around with it lots but my production stuff is all Gatsby/NextJs as I tend to do small/medium projects as a solo dev. So take this feedback with a grain of salt, probably not worth as much as someone who has a big project running in it. At first glance I really like your naming choices, except for "meta" - that brings up connotations of the company for me. You suggested "head" as an alternate; I wonder about specifying what it does - "seo" since this is the purpose of that rendering mode. I actually find the breakdown of "server" and "client" easier to understand than "ssr" and "csr". You could do "server" and "browser" as well but I think that is a toss up for me, client and browser both work well. I know when I first getting back into webdev a few years ago the client, server, ssg, ssr, isr lingo all really confused the hell out of me. One of the use cases I have simmering in the background are some ideas around a fully integrated electronic health record and user facing website - think Shopify but targeted at psychologists, social workers, and other allied health professionals (OT, SLP, PT, etc.). This is my actual field of work (mental health) and the software we have to use is AWFUL. I have picked away at this idea a bit both using Redwood and NextJS. The use case I am most curious about is mixing a marketing site (mostly static, should be SSG) and an app site (mostly dynamic, should be CSR or SSR). Right now it seems like it might be easier to build the "app" in Redwood and the marketing site in NextJS. Which I know is an option but I like where this proposal is heading, and especially if we get the option to add more "sides" in the future it really improves the DX for these kinds of situations. The other thing reading through your info got me thinking about was i18n - the last project I completed we had to handle a few different languages, and multiple languages per region/locale. I used middleware from NextJS to deal with this and it was a good UX and DX doing it that way. Is this sort of like what the server hooks would achieve? Intercept the request and do something based on that request? This is how I imagine using them anyways. Keep up the great work!! |
@joseph-lozano Thank for bringin this up and its a good point. The default rendering mode will be client (like it is right now). The idea behind it is that you only opt-in to more complexity, only when you need it. As far needing a FE server - the server isn't something you'll have to configure:
It is more complexity, but not much - in every case all you need to do is setup a deploy with @ehowey - thanks for the feedback, very useful! Appreciate you spelling out your thoughts in such detail ✌️
Yes - you could use serverData hooks to achieve the same. Using edge functions/middleware might still be useful though - because these functions run closer to your user. Maybe when renderModes is in alpha I could ask you to take it for a spin for this use
This is exactly the use case render modes are perfect for. |
@dac09 Yeah, after thinking about it a little, I came around that a FE server is probably necessary for RSC(which is why I deleted that part of the comment, though I probably should have just edited it). I still find it strange that the request will go from the browser -> FE server -> API Server -> Database before making the round trip. I am not running Redwood in production, but some additional concerns are how to scale the FE server separate from the API server (on render and fly), and whether this would significantly increase costs (for serverless deployments including Vercel) I wonder if it would be feasible for this to be something to opt out of entirely (i.e., run a static FE instead of a server FE) or if that would increase the matrix of deployment options past unmaintainability. |
Nice work! I can drop some thoughts as I read through.
routesHook feels cleaner to me. A function with the signature like in your example. Maybe generalize and call it
I hadn't considered only rendering the meta tags server side. Though for the Open Graph use case, I might still reach for full server-side rendering. Currently when I'm rendering Open Graph tags, I end up using the same (or similar) query in the page body. It would feel wasteful to stop at |
This is really great insight @banjeremy! I agree that the However, this begs a couple of questions I'm struggling with:
Perhaps we shouldn't have meta rendering in the first iteration, and add it when we discover more usecases.
I'm wondering if you have any thoughts on this? Just to note however, I think Cells make total sense conceptually in the server components/streaming world.... feels like we're a little in limbo here with the two patterns developing in the React ecosystem. More updates/clarification on this later today! |
Updates on Mon 14 Nov 22: Changelog and explanation below: Deployment partner support
It looks like you can configure netlify to work with a server (see Remix https://docs.netlify.com/integrations/frameworks/remix/) When/how Server data is triggered
After a bit more research with other frameworks and workflows - Next, Remix, vite-plugin-ssr - I’m leaning towards keeping the concept the same. Different scenarios for when/how the
Clarifications on how auth will workIt makes sense to centralize the auth validator logic in the top level app.routeHooks.ts file. Example flow:
✅ This means that we don’t need to repeat the auth verification code in every routeHook
❓ I still need to look into more how we can share the auth validation logic with the api side |
Updates on 13 Dec 22: I have a little more clarify after having spent some more time on this with Kris and Tobbe, and just letting the ideas percolate a little! Are serverData hooks just for making GraphQL queries?I finally have a good example of where you would use a If you are hosting your content on an external CMS (like Contentful, Storyblok, Strapi) - this data has to be fetched from some sort of an API. This is where Prepopulating Cell data using the server data hookWhen you use Remix’s data loaders, or Next’s a) If you are navigating to the URL for the first time (or a hard refresh), the flow would look like: So the browser essentially waits till both the b) You just clicked on a link on a page, so it does client side navigation. In this case, your data has to be fetched using an async API call (i.e. fetch) What’s happening here is that your So how would you get your Cell content as a serverData hook? You need to “register” your query in the server data hook: // DashboardPage.routeHooks.ts
import { QUERY as MyCellQuery } from 'src/components/MyCell'
import { makeGqlQuery } from '@redwoodjs/web'
export const serverData = ({ params }) => {
// 👇 Key step
const cellData = await makeGqlQuery(MyCellQuery, { variables: { id: params.id } })
const content = await fetchTextFromCMS('dashboard.content.main')
return {
data: {
content,
// 👇 pass it through here, name of variable TBC
cellData
}
}
} When you Cell is rendered on the client side, internally it will check if it’s data is already present, and render the success component. But why aren’t you just rendering all Cells on the server? One of the things we’re trying to achieve is to the fetching-data-step independant of the rendering-step, so that we can fetch your data using an API call when doing client side navigation. i.e. Get your Cell data, without having to render the whole page upto and including the cell. If you do not add the lines to cache your cell query, your Cell will be rendered in the loading state by default, and fetch data from the browser (like it does now). Why bother rendering the Cell at all? In the majority of cases when you want a dynamic meta tag (e.g. the title of the page changes based on the article’s title) - you want to use the data that you’d normally use your Cell to fetch. Why have a Cell in the first place? In theory I could pull data directly from my database in the serverData hook Totally valid question - the main advantage of having your data exposed via the GraphQL API is that it works for more than one client. If you have multiple web apps, or a mobile app, this data remains easy to access! If you had your data queried directly via the database, you’d have to expose the data via GraphQL or a custom function! Questions for you
|
Big UpdateDanny & Co. have a released an experimental version of Render Modes. You can check out the documentation and take it for a spin over here on the Forums: Feedback wanted! |
👋 Hello Redwood community, this is your chance to shape one of the major features in upcoming versions of Redwood! Let's see if we can build this feature together!
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:
<head>
tagsProposed render modes are:
static
- render this page as pure HTML with no JS tags, on every request to the serverserver
- 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/discordstream
- this depends on React 18, and there’s a lot more to understand here before we make it officialThe 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 anddataLoaders
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.This has two parts to it:
serverData
- a function defined in yourpage.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!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 theuseServerData
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.
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
IV. Outstanding Conceptual Questions aka Help wanted!
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
b) In the routesHook file
In both cases, we could abstract the specific header names - but not sure if there’s much value in abstracting them.
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.
Do they make sense to you? Does it sufficiently describe the choice you are making? Some other terms I’ve considered:
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 HookThis would require the router to support nested routing - which has certain pitfalls we've intentionally avoided for a while.
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?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:
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!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.
I’m not clear on this - and I worry that we may make it confusing for users.See update on this below #6760 (comment)
V. Other Practical Things
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)
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).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:
static
routes from a frontend server with cache headers vs serving the same file from CDN.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.
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?
The text was updated successfully, but these errors were encountered: