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

WIP: RSC sub route #1858

Draft
wants to merge 23 commits into
base: v1.x-2022-07
Choose a base branch
from
Draft

WIP: RSC sub route #1858

wants to merge 23 commits into from

Conversation

wizardlyhel
Copy link
Collaborator

Description

RSC Sub routes - Allow Hydrogen to define sub routes with RSC:

Expected behaviour:

  • SSR generates content on server-side as a Suspense fallback
  • Client side hydrates with a __rsc call (for now)

Before submitting the PR, please make sure you do the following:

  • Read the Contributing Guidelines
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123)
  • Update docs in this repository according to your change
  • Run yarn changeset add if this PR cause a version bump based on Keep a Changelog and adheres to Semantic Versioning

@wizardlyhel
Copy link
Collaborator Author

@frandiox This branch is demonstrating how I think we can do an opted-in RSC sub route implementation without making it a major breaking change.

To play with this, just uncomment out <RSCSubRoute /> in the demo-store index.server.tsx. You will see it is trying to make an rsc call to /test path (which I haven't figure out how I can dynamically register a hydrogen route yet)

I notice that we virtually imports hydrogenRoutes in the handleRequest. Isn't this static? Do we need to re-import this on every worker request? or can we actually save this globally in memory?

I am thinking to dynamically register these sub routes as SSR render discovers them .. that is if we can guarantee to have a SSR render before an RSC request.

Another way is to have these sub routes to be also defined in the routes folder so that they also get discovered by the virtual import.

Thoughts?

@wizardlyhel
Copy link
Collaborator Author

@frandiox I updated this PR to demonstrate sub routes working. I think this solution is quite elegant - it doesn't make a major breaking change.

How it works

I put the sub route inside demo store's routes/component folder. Vite picks up the sub route with virtual file routes. Client-side makes the extra rsc call to the sub route.

Thinking about how to get it not make a second __rsc call on full page load

Wondering if I can do a RSC render (along with the fallback <Suspense fallback={cloneElement(page, serverProps)}> and output the rsc output inside in the meta tag with a useID generated id. Then we pass this id along with RSCSubRouteClient so that it can pick up the rsc output that got generated by SSR

@frandiox
Copy link
Contributor

frandiox commented Jul 19, 2022

Thanks for putting this together! I haven't had much time yet to play with it but, if I didn't understand it wrong, I think there are a few challenges here:

  • RSC payload in SSR is still in one big chunk so it cannot be reused later when navigating.
  • Even if we are able to split the main RSC payload during SSR in sub-payloads, they still wouldn't make it to the browser cache yet, so perhaps we'd need to use manual in-memory caching before calling fetch for navigation?
  • It seems there's a necessary RSC waterfall request when navigating. I assume this is not a problem for already-cached RSC payloads but it might not be trivial for new navigations. I guess the only way to prevent this is if the browser is aware of what it needs to request beforehand... but that would require some sort of manifest...

Any idea how to deal with any of this? 🤔


A possible approach to explore for the first challenge would be changing RSCSubRoute.server.tsx to do something like this:

  1. rscRenderToReadableStream(actualServerComponent)
  2. .tee()
  3. createFromReadableStream
  4. <>{response.readRoot()}</>
  5. save the other teed RSC stream in request object
  6. render each saved RSC stream to different meta tags during SSR.
  7. somehow, hydrate this in the browser later using all the different inlined sub-RSC payloads❔

I've no idea how this would affect CPU performance though.

@github-actions github-actions bot had a problem deploying to preview July 21, 2022 15:54 Failure
@github-actions github-actions bot had a problem deploying to preview July 21, 2022 17:32 Failure
@github-actions github-actions bot had a problem deploying to preview July 21, 2022 18:04 Failure
@blittle
Copy link
Contributor

blittle commented Jul 21, 2022

If we send to the client the separate layout maps, then the client should know during transitions which RSC request to make and which not to, so we wouldn't need to rely on browser cache.

@wizardlyhel
Copy link
Collaborator Author

WIP Sub route API proposal

All new function naming are not final

How to opt-in using Hydrogen sub route

  1. Most likely going to define a new entry-client so that the main page content is also a sub route implementation - I haven't fully hash this out yet

  2. Define sub routes

Global sub routes

Define global sub routes in App.server.tsx and make sure the outlet is included in renderHydrogen. These sub routes will be available on all routes

export const HeaderRSCOutlet = defineRSCOutlet({
  outletName: 'HeaderRSCOutlet',  // haven't figure out how I can avoid the need to have this string to match the export name
  component: Header,
});

function App({request}: HydrogenRouteProps) {
   return (
     <HeaderRSCOutlet />
   );
}

export default renderHydrogen(App, {
  HeaderRSCOutlet,
});

Page sub routes

Define page level sub routes in page routes, for example, routes/index.server.tsx. These are sub routes that will only exist for this page route.

const Test = ({id}: {id: string}) => (<p>Test RSC Outlet {id}</p>)
export const TestRSCOutlet = defineRSCOutlet({
  outletName: 'TestRSCOutlet',
  component: Test,
  dependency: ['id'],
});

export default function Homepage() {
  return (
      <TestRSCOutlet id="123"/>
  );
}

@blittle
Copy link
Contributor

blittle commented Aug 10, 2022

Define page level sub routes in page routes, for example, routes/index.server.tsx. These are sub routes that will only exist for this page route.

Wouldn't we want some sub-routes to share? Example from remix:
image

The global layout would include the left navigation, which is shared across the whole app (like our header & footer), but when on the sales tab, the top navigation is shared for all those "sub routes". How would I describe a tree like this with the proposed API?

Comment on lines 32 to 47
if (serverProps.outlet && component) {
console.log('RSCSub', serverProps.outlet, serverProps.response.cache);
serverProps.response.cache(cache);
return component ? component(state) : null;
} else if (isRSC) {
console.log('RSCSub - RSC');
return (
<RSCSubRouteClient outletName={outletName} state={state} isRSC={isRSC} />
);
} else {
console.log('RSCSub - SSR');
return (
<RSCSubRouteClient outletName={outletName} state={state} isRSC={isRSC}>
{component(serverProps)}
</RSCSubRouteClient>
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@frandiox I want to gather on your thoughts on this

So I have been noodling on this pattern for the outlets and I am wondering if it is do-able for the full page SSR as well.

What this code piece is doing ...

Say we have a server component

<Product handle={handle} />  => <p>Product title</p>

On SSR pass for /product/123, this will render the SSR output of Product and a RSCSubRouteClient. We don't need a RSC payload to hydrate because the children of RSCSubRouteClient is the exact content we need.

<RSCSubRouteClient state={{handle}}>
  <p>Product title</p>
</RSCSubRouteClient>

On RSC pass for /product/123, this will just render RSCSubRouteClient with the state

<RSCSubRouteClient state={{handle}} />

The content of this route <p>Product title</p> will be replaced by rscResponse.readRoot()

Do you think it is possible to do the same for the full page SSR render and we can drop the 2nd RSC pass entirely?

@github-actions github-actions bot had a problem deploying to preview August 17, 2022 22:30 Failure
@github-actions github-actions bot had a problem deploying to preview August 18, 2022 16:38 Failure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants