Streaming without suspense in apollo nextjs client

Hey everyone, I was wondering if there has been any thought around implementing server side streaming in apollo/next.js like you get with useSuspenseQuery, useReadQuery etc while maintaining the old useQuery API/not using suspense boundaries everywhere. I don’t know if this sounds really dumb but I wrote a proof of concept here: https://github.com/andrew-d-jackson/apollo-next-suspenseless-streaming that seems to work, though it’s very quick and messy. There is a writeup in the readme of how it works.

I might be an old grump but I don’t like the new suspense APIs for data fetching, not just with apollo but with everything, they are much more verbose and you end up with a mess of useTransitions and fallback components. At the same time streaming data is great and I want the benefits of it: quick initial UI to the client and starting to fetch data before they load + request. In my head and streaming data from the http request are different things and we can do one without the other. Have the old useQuery api where it’s just {loading, data, error} = useQuery() and stream the data over initially.

I feel like I’m missing something big, and I’m mostly writing this in the hopes that someone will tell me what I’m missing, but my proof of concept seems to show that it’s at least possible. Has there been thought about doing things that way?

Hi Andrew,
we do have streaming support over in GitHub - apollographql/apollo-client-nextjs: Apollo Client support for the Next.js App Router, but we are explicitly only targetting suspenseful apis there.

The main reasoning for this is SSR: Next.js only does one SSR pass, and if you use useQuery, that means that it will always render a “loading” state and then stream that loading state to the browser.
With an approach like yours, you can keep the stream open and at least deliver the payloads to the browser soon after, but from a user perspective, they are still getting a “loading” state as the SSR result that will then hydrate and later render into the real UI state.

With suspense on the other hand, the server streams UI chunks over as they are ready - without a loading state in-between. And if things start taking too long, you get to decide which parts of your UI hydrate together/go into a loading state together.
That’s not only much better for SEO (the search engine will get the full page contents without ever having to enable JavaScript), it usually also makes for a better user experience - but of course I agree that the mental model needs quite a shift during development.
On user experience with Suspense, I’d highly recommend you this talk from my colleagues @jerelmiller and @alessbell: How to Use Suspense and GraphQL with Apollo to Build Great User Experiences - GitNation

Hey, thanks for the reply.

I understand the benefits of suspense, I think there is still benefit to what I am talking about though in that you don’t need to refactor anything, and you get better initial load for existing SPA codebase for basically free (apart from the below).

After doing a lot more reading around the internet, it turns out tanstack query actually implements what I am talking about, it’s the only major library I could find that does: Advanced Server Rendering | TanStack Query Docs (scroll to bottom) the reason they recommend against it is because while it works for initial page load, on page changes (in next at least) you can fallback to 2 requests before data, because you have to request the js chunks for the next page, then run them, then request data. But this is a tradeoff that could be worth it in some scenarios, they seem to think so.

If you mean the heading “Experimental streaming without prefetching in Next.js”, that is very close to a reimplementation of our package, and as I understand it, it also requires users to use suspense hooks.

Their criticism on request waterfalls is essentially a REST concern - in a GraphQL world you can fully avoid that with fragment compoisition. (But we are planning on adding more prefetching mechanisms nonetheless, and of course everything is always a tradeoff.)

and you get better initial load for existing SPA codebase for basically free

Can you explain that a bit? To me it looks like it would make the initial load a lot slower.

As I understand it, your current solution blocks all client requests until the server has fully finished, and then you flush over all the requests and responses at once and hydrate the client. In my eyes, that seems to fall the “renderToStream” behaviour almost back to a “renderToString” approach - while usually with the Next.js app router (and yes, suspense), the UI streams in as soon as pieces of your UI are done, resulting in a faster page load, this would essentially lock the page into a loading state until everything has been processed on the server.
One slow response could lock your whole page for a long time.

If you mean the heading “Experimental streaming without prefetching in Next.js”, that is very close to a reimplementation of our package, and as I understand it, it also requires users to use suspense hooks.

Looking back at it your right, I’ve misunderstood what they were saying completley. Looks like nothing implements what I am talking about, haha.

Can you explain that a bit? To me it looks like it would make the initial load a lot slower.

I don’t mean faster than suspense streaming, I mean faster than an SPA where you don’t stream at all. You don’t need to make 2 requests to the server on initial load (one for the js, run it, then one for the request), so you should in theory get the query responses back quicker.

As I understand it, your current solution blocks all client requests until the server has fully finished, and then you flush over all the requests and responses at once and hydrate the client.

I am hydrating react/the dom before I get the responses, but I hydrate (reset?) the apollo cache after I get them. So users could interact with non-apollo things while waiting for a long query to be streamed.

In my eyes, that seems to fall the “renderToStream” behaviour almost back to a “renderToString” approach - while usually with the Next.js app router (and yes, suspense), the UI streams in as soon as pieces of your UI are done, resulting in a faster page load, this would essentially lock the page into a loading state until everything has been processed on the server.

I think that could be an implementation detail of my approach maybe. You could in theory stream each query result as it’s finished on the server piece by piece and update the apollo cache one by one. As well as send over the list of queries being fetched on the server upfront and just pause those on the client until they are streamed down instead of pausing everything. I might have a go at implementing this but I’m not very familiar with the apollo internals on how to update the cache and re-render queries one by one so it might take me a while.

Even with my current implementation though its still faster than render to string in the sense that you can the loading state ui and hydrate the dom before the graphql queries have finished.

Again I’m probably misunderstanding something, I really appreciate you taking the time to respond and your thoughts.

Hmm.

To be honest, you really don’t want to go reimplementing our package - it really is a pain, so I’m trying to think of ways to save you from that :slight_smile:

To get to the result that you want to get, it might be possible to use our package and add the capability to hotpatch the useQuery hook itself to behave more like the useSuspenseQuery hook during SSR.

That way, you could start with a useQuery only application and once that’s in place, it would behave very similar to what you currently do.
Then, a user could add Suspense boundaries to their application to insert “hydration boundaries” and slowly migrate over to useSuspenseQuery in parts of their application to get the suspense feeling in the browser.
It would never be a default, and always opt-in (and documented more for migration purposes than a long-term solution), but I could imagine it working.

I did a little experiment in that direction here: [experiment/idea] allow enabling "forced suspense" for useQuery hook in SSR by phryneas · Pull Request #226 · apollographql/apollo-client-nextjs · GitHub

Maybe you can try the PR release a try? :slight_smile:

Just out of interest - did you try it out?

Hey, sorry for the really late reply, I tied setting it up on the example site in my repo but it looked like it would still make requests from the client on initial load, I probably just set it up wrong though. Why is " globalThis.hydrationFinished?.();" used in your test? That’s probably what I did wrong.

It doesn’t seem like there is much demand for this though and it’s probably not worth adding to apollo, I think it’s a useful proof of concept to show that you can do streaming without using suspense that someone might find interesting but I think the benefits of it are probably only worthwhile in a small number of circumstances (You have an app that doesn’t care about javascript bundle size or SEO but you do care about initial api request time and you don’t want to use suspense).