SSR with Meteor.callAsync

I use both of these ways.

You can also check the updated React Suspense in React 18

this no longer works.

You would need react server components for that, which is not supported by meteor.

Using Fiber’s in react server rendering was also not intended by react and lead to bugs in the past, altough it kindof worked.

If SSR is important, you may consider moving to a SSR framework like nextjs which supports server components. you can still keep meteor just for the api/backend

I don’t think so. SSR and RSC are 2 different things.

@grubba, @denyhs, can you provide some guidance on the future of React SSR with Meteor.callAsync()?

Is there an existing solution that allows Meteor methods to be the data source for React components for server-side rendering of those components?

Caveat: there are now “multiple versions” of SSR in React

  • sending the complete HTML before hydration
  • sending the HTML shell, hydrating, and then loading the rest of the component client-side
  • sending the HTML shell, hydrate shell, load server-rendered contents (RSC), and hydrate those contents one by one as they come

yes, but if you want to render components based on async data sources you either have to:

  • a) use server components
  • b) abstract your async call in a hook and continuously rerender the whole tree on the server until all your async calls are fullfilled. This is what apollo-client is doing when you want to server-render its data.

a) is not supported and b) needs a library that supports that pattern (or you write the nessecary abstraction yourself)

1 Like

If I understood correctly, we can fetch this data in an SSR environment(with suspense). We have published the suspendable hooks.
I think using the useTracker with suspense in this scenario should work. Maybe @radekmie has some other ideas of how to make Meteor.callAsync work properly in an SSR environment.

SSR these days in react seems a little bit complicated :confused:

@grubba, we are hoping that this will work with Meteor.callAsync()

Before Meteor 3.0, it was as simple as (updating the OP’s code):

const Component = () => {
    const [data, setData] = useState("default")
    
    const serverResult = Meteor.call(name, args, (error, clientResult) => setData(clientResult));
    
    return serverResult || data;
}

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

This is the current solution I am using: GitHub - phibya/meteor-ssr-async

I’m not necessarily up-to-date with the latest React SSR state, but my take is that:

  1. You don’t need React Server Components.
  2. Using Suspense should work just fine, and there’s nothing Meteor-specific here.
    • Any Promise can suspend a component, but the management of throwing and caching them around is kind of tricky. We solved it for, e.g., useSubscribe, just as @grubba said, but Meteor.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.
  3. @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).
  4. 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 call pipe 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 of onShellReady 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:

  1. Suspense and lazy did not work in SSR when not yet cached by the server
  2. renderToString() is not meant to wait for data

Screenshot from 2023-12-22 20-05-17

Potential solution:

  1. Make data fetching Suspense-enabled, i.e., throw promises when data is not yet ready
  2. 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?

3 Likes

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:

  1. Meteor 2.14
    • Using Meteor.callAsync()
  2. React Router 6.21
  3. React 18.3.0-canary-c5b937576-20231219

The example in the above branch has a component and data loaded using Suspense and nested lazy loaded components.

Screenshot from 2023-12-24 02-53-23

Caveats:

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:

  1. 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.
  2. Allow access to the response parameter accessible by renderToPipeableStream()

Ideally, these features will be available through server-render package.

2 Likes

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