Unfortunately using Meteor.callAsync().then(setData)
currently does not work for me (with Meteor version 2.14) as renderToString
does not wait for any Promises to resolve. As for now, I am writing a custom hook for data retrieval on the server side and making a loop to keep rendering until all promises are fulfilled. This is the same as @macrozone mentioned above (solution b).
Care to share your solution @pbya? At the very least, it might become an inspiration for a core solution
I’m not necessarily up-to-date with the latest React SSR state, but my take is that:
- You don’t need React Server Components.
- Using Suspense should work just fine, and there’s nothing Meteor-specific here.
- Any
Promise
can suspend a component, but the management ofthrow
ing and caching them around is kind of tricky. We solved it for, e.g.,useSubscribe
, just as @grubba said, butMeteor.call
would need a separate logic. - In the meantime, you could something like this package (I didn’t use it myself; just searched for “react suspense use promise”).
- In the future,
use
may simplify it.
- Any
- @grubba tried using Suspense in Meteor SSR and it worked as expected, i.e., sent a “shell” immediately and streamed data when it was ready.
- I remember there was also an option to wait for everything before sending anything (i.e., no streaming).
- This package @pbya suggested is kind of like Suspense, but globally, i.e., there’s no intermediate shell being sent to the client. (There’s also no error handling whatsoever.)
all those approaches mean that whatever you get from the Server is NOT rendered with the data already present, or isn’t it?
Suspense will send a ready HTML (i.e., with the data) if it’s loaded “fast enough” (I don’t think it’s not definied directly) or an empty shell (i.e., loading screens).
Here is the relevant function in React 18: renderToPipeableStream
There are two (2) important options:
Option 1:
- optional
onShellReady
: A callback that fires right after the initial shell has been rendered. You can set the status code and callpipe
here to start streaming. React will stream the additional content after the shell along with the inline<script>
tags that place that replace the HTML loading fallbacks with the content.
Option 2:
- optional
onAllReady
: A callback that fires when all rendering is complete, including both the shell and all additional content. You can use this instead ofonShellReady
for crawlers and static generation. If you start streaming here, you won’t get any progressive loading. The stream will contain the final HTML.
We still have not moved to React 18, with Meteor 3 being a blocker for us, so we have not tested these yet.
Looking at this, it looks like this requires support from webapp e.g. allowing pipe()
function and response
object to be accessible
Looking at this further, there is a need to extend/create a new boilerplate that will support renderToPipeableStream
of React 18
Here is a quick reproduction where SSR does not work:
- still using
Meteor.call()
- Meteor 2.14
- Reach 18.2
- React Router 6.21
- using Suspense and lazy
- using
renderToString()
The goal is to support the following through SSR and code-splitting:
- loading performance
- better UX
- SEO
It looks like it works because the app works after hydration. Disabling javascript in the browser shows the real status of the SSR:
- Suspense and lazy did not work in SSR when not yet cached by the server
renderToString()
is not meant to wait for data
Potential solution:
- Make data fetching Suspense-enabled, i.e., throw promises when data is not yet ready
- Support
renderToPipeableStream()
by forking the following Meteor packages:
server-render
boilerplate-generator
webapp
I will post an update here if this will work. Do you have other ideas/comments/feedback?
While studying renderToPipeableStream()
, it becomes clear that React 18 expects to start hydration on document
(therefore the entire HTML document) instead of an element inside <body>
.
This means that the boilerplate template for React 18 SSR must also be JSX.
Good News!
SSR works with the following:
- Meteor 2.14
- Using
Meteor.callAsync()
- Using
- React Router 6.21
- React 18.3.0-canary-c5b937576-20231219
- Suspense
- lazy
renderToNodeStream()
use()
- use – React (as mentioned by @radekmie above)
The example in the above branch has a component and data loaded using Suspense and nested lazy
loaded components.
Caveats:
- Still using
renderToNodeStream()
, which has been deprecated in React 18
use()
hook is still in the canary channel (expected to arrive in 18.3) - can be used in production by pinning the canary version
The use()
hook simplifies everything with Meteor.callAsync()
and Suspense.
const { links } = use(Meteor.callAsync('getLinks'));
To Do:
Since renderToNodeStream()
is deprecated, there is still a need to support renderToPipeableStream()
moving forward. There are two important things to do to make this possible:
- Support a JSX boilerplate-generator template because hydration now happens on
document
instead of an HTML element. This requires allowing a custom template to generate the HTML output. Either Meteor adds a custom template for React or allows the developer to add one. - Allow access to the
response
parameter accessible byrenderToPipeableStream()
Ideally, these features will be available through server-render
package.
So, I’ve just proven myself wrong. With the solution above, it’s possible to even make this work with class components with data fetching happening in the constructor()
when defining the component state
variables
Any updates to this??
In other words, how do you make SSR work with Meteor 3?
I have previously used it, but there was an issue for my own case that I had struggled, so I implemented my own.
Here is my initialization at server startup:
Here is all the components I use in SSR:
@minhna My concern is not being able to do the Meteor.callAsync
, in the referenced ssr components file. I tried, not working…
This has been the single blocker for me preventing to upgrade to Meteor 3.
Any advice is welcome, thanks!
The following should work:
- Meteor 3
- React 18 canary for the
use()
hook
The use()
hook allows you to await the Meteor.callAsync
method and suspend the rendering
Great! Thanks for this. Will try soonest and update here. Meanwhile, if you can share a piece of code as example, it would be awesome.
I was trying to upgrade my example above to Meteor 3 but was met with package conflicts. I don’t have time to figure out the issue for now so I don’t have an example to share.
We are also in the middle of upgrading our SSR code to Meteor 3 (on hold due to new business requirements we have to prioritize). We are currently moving data fetching as a parent of SSR components to allow suspending the entire component with use()
Please share if you have something to show. I’ll do the same thing once I get the opportunity