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

RFC: React 18 SSR + Suspense Support #10231

Closed
Tracked by #1723
jerelmiller opened this issue Oct 25, 2022 · 67 comments
Closed
Tracked by #1723

RFC: React 18 SSR + Suspense Support #10231

jerelmiller opened this issue Oct 25, 2022 · 67 comments

Comments

@jerelmiller
Copy link
Member

jerelmiller commented Oct 25, 2022

This RFC replaces the previous conversations on React 18 SSR (#8365) and React Suspense (#9627).

Background

React 18 brings some new capabilities to SSR primarily with the help of React suspense. Among these is the ability to handle streaming HTML with the newly introduced renderToPipeableStream function. This relies on Suspense to determine which fallback boundaries will be returned with the initial HTML chunk.

You can find information about the architecture in this discussion along with a demo of this functionality created by the React team here. An upgrade guide is also provided to give an idea of how these APIs are used.

Technical guidance

Adding proper React 18 SSR support means adding support for React suspense (#9627). Once we build out our suspense solution, this should get us most of the way to SSR support. At this time, it's unclear to me what will be lacking in the SSR context once we implement SSR. We will be able to learn this once we start work on the feature.

Here are some ideas on how we might approach this from a high-level technical/API standpoint.

API

Suspense-enabled queries should be opt-in. Because React requires a <Suspense /> boundary to properly work when a component suspends, there will need to be code modifications. Allowing developers to opt-in would allow the developer to add suspense support at their own pace.

We want to enable the use of suspense by introducing a new hook called useSuspenseQuery:

useSuspenseQuery(MY_QUERY, options)

Using useSuspenseQuery will require use of a suspense cache to keep track of in-flight requests. This will be instantiated and passed to the <ApolloProvider /> component.

const suspenseCache = new SuspenseCache();

<ApolloProvider client={client} suspenseCache={suspenseCache}>
 {children}
</ApolloProvider>

Supported options

The new useSuspenseQuery hook aims to feel similar to useQuery, so it should operate with a similar set of options. Below is the list of useQuery options that we plan to support with useSuspenseQuery

Existing options

useQuery option useSuspenseQuery option Notes
query
variables
ssr Ideally useSuspenseQuery would "just work" with SSR so we'd like to avoid this option. We will re-evaluate with feedback over time.
client
errorPolicy
onCompleted Because the component suspends, this option is an oddball. Until we have a use-case, we won't support it.
onError Because the component throws errors, this option is an oddball. Until we have a use-case, we won't support it.
context
returnPartialData
canonizeResults
defaultOptions Pass the option directly
fetchPolicy
nextFetchPolicy Update the fetch policy by passing a new policy directly in options
refetchWritePolicy
pollInterval
notifyOnNetworkStatusChange
skip
partialRefetch (deprecated)

Notable changes in behavior from useQuery

Using a suspense query will differ from useQuery in the following ways:

  • There is no loading state

With a suspense enabled query, a promise is thrown and the nearest <Suspense /> boundary fallback is rendered. Because suspense now handles the loading state for us, we no longer need a loading boolean returned in the result.

  • data should no longer be undefined

A pending promise in a suspense query will suspend the component (i.e. throw the promise), so we can guarantee that once the data is resolved successfully, we have non-null data. Using suspense will allow a developer to remove !data checks in render.

// Now you can just use `data` as if you had it all along!
const MyComponent = () => {
  const { data } = useSuspenseQuery(MY_QUERY);

  return (
    <div>
      {data.map((item) => <div key={item.id}>{item.value}</div>)}
    </div>
  );
}

Caveat

When using the skip option, or when you have used a different errorPolicy than the default, data may still be undefined. This principle only applies to the default behavior.

  • Multiple useSuspenseQuery hooks in the same component will result in a request waterfall

As a best practice, we should avoid recommending the use of multiple useSuspenseQuery hooks in the same component. useSuspenseQuery will suspend immediately, which means calls to other useSuspenseQuery hooks in the same component won't run until previous calls have been resolved.

const MyComponent = () => {
  const { data: data1 } = useSuspenseQuery(QUERY_1); 
  const { data: data2 } = useSuspenseQuery(QUERY_2); // won't fetch until the first result resolves

  return (
    <>
      <div>{data1.myQuery}</div>
      <div>{data2.myOtherQuery}</div>
    </>
  );
}

Instead, we should recommend using separate components surrounded by a suspense boundary to fetch data in parallel.

const MyOuterComponent = () => {
  return (
    <React.Suspense fallback="Loading...">
      <MyFirstInnerComponent />
      <MySecondInnerComponent />
    </React.Suspense>
  );
}

const MyFirstInnerComponent = () => {
  const { data: data1 } = useSuspenseQuery(QUERY_1); 

  return <div>{data1.myQuery}</div>;
}

const MySecondInnerComponent = () => {
  const { data: data2 } = useSuspenseQuery(QUERY_2);

  return <div>{data2.myOtherQuery}</div>;
}
  • Error policies

As encouraged by some of the early suspense docs, rejected promises will result in errors, which means an error boundary should be used to capture the error state. If a useSuspenseQuery fulfills with a rejected promise, we throw that error. You can see an example of this behavior in a demo provided by Kent C. Dodds via an Egghead tutorial.

Though we will throw as the default behavior, we want to enable users to have control over how errors are handled. We should respect the error policy set by the user.

Error policy Throws the error Notes
none
ignore 🚫
all 🚫 As with useQuery, this will allow you to get partial data results alongside the error via the error property returned by useSuspenseQuery
  • We no longer need getDataFromTree

With the new architecture, we no longer need our 2-pass rendering approach as React 18 SSR uses streaming HTML and suspense boundaries. We can no longer rely on this behavior since rendering is no longer synchronous while using renderToPipeableStream.

We should consider deprecating this function (and the related renderToStringWithData) and encourage others to migrate to renderToPipeableStream. Perhaps this is something we ultimately move to a separate bundle for backwards compatibility in a future major version of Apollo for those that need support for synchronous rendering.

  • Investigate if we can deprecate the ssrMode option in ApolloClient

I don't have a lot of context for what ssrMode does under the hood, but this might be an opportunity to deprecate this flag in our options to the ApolloClient class. If we can pull this off, this gets us a step closer to allowing user to share an Apollo client between the server and the client.

Working with the cache

How we return data from a suspense-enabled query depends on the fetch policy specified for the query. When using a fetch policy that reads from the cache, avoid suspending the component and return cached data if the data is already in the cache. For policies that avoid the cache, always suspend the component.

Fetch policies

Supported fetch policies
Fetch policy Supported?
cache-first
cache-only
cache-and-network
network-only
no-cache
standby
Fetch policy behaviors
Fetch policy Description
cache-first Try to read from the cache. Suspend when there is no cached data.
cache-and-network Try to read from the cache while a network request is kicked off. Only suspend when there is no cached data.
network-only Always suspend when a network request is kicked off
no-cache Always suspend when a network request is kicked off

Building a promise cache

We will need a way to cache the promises thrown by useQuery when the hook is run. Apollo has the ability to deduplicate queries across components, which means it's possible more than one component by rely on the same promise to resolve. We need a way to associate queries with their promises so that if components are rendered, we can look them up and throw them if necessary to suspend the component. React 18 concurrency features also may determine that a component should be re-rendered at any time. We want to ensure any suspended component that attempts to be rendered is able to properly look up a pending promise and re-throw if necessary.

We will need this to be a React-only feature, so adding this to something like QueryManager won't work since its part of core. Perhaps this is something we consider initializing in ApolloProvider.

There is a RFC for a built-in suspense cache, but this is still a ways off. We will need to build our own until this is in place.

This is also particularly important if we want to enable the render-as-you-fetch pattern in Apollo.

Usage with @defer

useSuspenseQuery should aim to take advantage of the benefits @defer provide, namely being able to render UI when the first chunk of data is returned. Because of this, we should avoid suspending for the entirety of the request, or we risk negating the benefits of the deferred query. Instead, we should only suspend until the first chunk of data is received, then rerender as subsequent chunks are loaded. We also plan to add support for suspense in useFragment to allow deferred chunks to suspend as they are being loaded.

Roughly, this should work as follows:

const QUERY = gql`
  query {
    greeting {
      message
      ... @defer {
        recipient {
          name
        }
      }
    }
  }
`;

// 1. Issue the query and suspend
const { data } = useSuspenseQuery(QUERY);

// 2. When the initial chunk of data is returned, un-suspend and return the first chunk of data
const { data } = useSuspenseQuery(QUERY);
/*
data: {
  greeting: {
    __typename: 'Greeting',
    message: 'Hello world'
  }
}
*/

// 3. Rerender when the deferred chunk resolves
const { data } = useSuspenseQuery(QUERY);
/*
data: {
  greeting: {
    __typename: 'Greeting',
    message: 'Hello world',
    recipient: {
      __typename: 'Person',
      name: 'Alice'
    }
  }
}
*/

Usage with fetch policies

If using a fetch policy that reads from the cache, we should still try and read the entirety of the query from the cache. If we do not have the data, or only have partial data, fetch and suspend like normal. Leverage the returnPartialData option if you'd like to avoid suspending when partial data is in the cache.

Error handling

Error handling is trickier since the initial chunk could succeed while deferred chunks return with errors. I propose the following rules:

  1. If the initial chunk returns an error, treat it the same as if the query was issued without a deferred chunk. The behavior would depend on the errorPolicy set in options (see above for more detail on error policies).
  2. If an incremental chunk returns an error, collect those errors in the error property returned from useSuspenseQuery. Throwing the error will depend on the errorPolicy.
  • When the errorPolicy is set to none (the default), discard any partial data results and throw the error.
  • When the errorPolicy is set to ignore, discard all errors and return any data collected thus far (this might result in an incomplete query).
  • When the errorPolicy is set to all, add all partial data results and collect all errors in the error property.

SSR

The above implementation should get us most, if not all the way, there for SSR support. The one real outstanding question is how we populate the cache client-side once SSR completes so that we can avoid a refetch. See the Outstanding Questions section below for more information on this question.

Other considerations

Render-as-you-fetch pattern

Up to this point, our Apollo client supports the fetch-on-render pattern, which might introduce request waterfalls depending on how an app is structured. With the introduction of Suspense, we should be able to enable the render-as-you-fetch pattern, which allows data to be loaded ahead of time so that we can begin to render a component as data is being fetched.

This is a pattern we should explore as its now the recommended approach since it allows us to show content to the user sooner and load data in parallel.

Outstanding questions

How do we hydrate the client cache with data fetched server-side?

Our current solution fetches all data in a 2-pass approach via getDataFromTree. Using this method, we are able to detect when all queries have resolved before we send the rendered HTML. Because we are using synchronous rendering APIs, we are able to detect when rendering is complete and send the markup complete with the extracted cache data on a global variable.

React 18 makes this a lot trickier as the new architecture allows for the ability to stream HTML while the client begins to hydrate. Waiting for React to fully finish streaming the HTML in order to restore the client Apollo cache feels too late as its possible hydrated components may already begin to execute on the client.

On the flip side, React 18’s new renderToPipeableStream does include an onShellReady callback but it appears this might fire too early. From the docs:

    onShellReady() {
      // The content above all Suspense boundaries is ready.
      // If something errored before we started streaming, we set the error code appropriately.
      res.statusCode = didError ? 500 : 200;
      res.setHeader('Content-type', 'text/html');
      stream.pipe(res);
    },

I come across this discussion with some interesting comments about data hydration:

Hydration Data

You don't just need code to hydrate. You also need the data that was used on the server. If you use a technique that fetches all data before rendering you can emit that data in the bootstrapScriptContent option as an inline script. That way it's available by the time the bootstrap script starts hydrating. That's the recommended approach for now.

However, with only this technique streaming doesn't do much anyway. That's fine. The purpose of these guides is to ensure that old code can keep working, not to take full advantage of all future functionality yet.

The real trick is when you use Suspense to do you data fetching during the stream. In that case you might discover new data as you go. This isn't quite supported in the React 18 MVP. When it's fully supported this will be built-in so that the serialization is automatic.

However, if you want to experiment with it. The principle is the same as the lazy scripts. You start hydrating as early as possible with just a placeholder for the data. When the placeholder is missing data, and you try to access it, it suspends. As more data is discovered on the server, it's emitted as script tags into the HTML stream. On the client that is used as a signal to unsuspend the accessed data.

I believe this is something we will continue to learn about as we begin implementation.

@benjamn has proposed an interesting idea of potentially transmitting the whole query results produced via SSR rather than normalized cache results that would need to be serialized. Queries might then be able to use these query results immediately which would then make their way into the cache. This is something we should consider via an ApolloLink.

What other hooks allow a component to suspend?

useLazyQuery

We have this example in our docs:

import { gql, useLazyQuery } from "@apollo/client";

const GET_GREETING = gql`
  query GetGreeting($language: String!) {
    greeting(language: $language) {
      message
    }
  }
`;

function Hello() {
  const [loadGreeting, { called, loading, data }] = useLazyQuery(
    GET_GREETING,
    { variables: { language: "english" } }
  );
  if (called && loading) return <p>Loading ...</p>
  if (!called) {
    return <button onClick={() => loadGreeting()}>Load greeting</button>
  }
  return <h1>Hello {data.greeting.message}!</h1>;
}

This implies that the general usage of useLazyQuery is in response to user interaction. Because of this, I'm inclined to say that we not add suspense support for useLazyQuery and let it operate as it does today to allow for a better user experience. If we decided to suspend the component, this would result in the already displayed UI being unmounted and the suspense fallback displayed instead. This seems to be more in line with how startTransition works within suspense as it avoids rendering the suspense fallback when possible.

That being said, I wonder if we should consider using startTransition to allow React to determine whether to suspend or not (if possible). Per the docs:

Updates in a transition will not show a fallback for re-suspended content, allowing the user to continue interacting while rendering the update

Its unclear to me how exactly this works if the same hook allows the component to suspend, but would be interesting to explore nonetheless.

useMutation

This is a write operation and therefore should not suspend a component. Mutations are typically used in response to user interaction anyways. This behavior would be consistent with a library like Relay which only uses suspense for queries.

Existing SSR support

Our current solution uses an exported function called getDataFromTree that allows us to use a 2-pass rendering approach. This relies on React synchronous rendering via renderToStaticMarkup and attempts to wait until all data is fetched in the React tree before resolving (renderToString usage is also available via renderToStringWithData)

This is NOT affected in React 18 and will continue to work, though it is not the recommended solution. In React 17, any use of Suspense with renderToString or renderToStaticMarkup would result in an error. In React 18, this changed and these functions now support very limited Suspense support.

You can see a the existing functionality working with React 18 in a demo I put together.

Communication

React 18 SSR suspense support (and broad data-fetching support) is still very experimental. There are no formal guidelines yet given by the React team, so adding support means we will need to be reactive to breaking changes within React itself. Adding suspense to Apollo however might encourage the React team to move this functionality out of beta.

Once we go live with this functionality, assuming the React team hasn't introduced formal guidelines, we will need a way to communicate this to our broader audience. Below are some references to how other libraries do this:

I particularly like the way React Query states this:

These APIs WILL change and should not be used in production unless you lock both your React and React Query versions to patch-level versions that are compatible with each other.

Support for suspense in other libraries

This shows the current landscape of client suspense + SSR suspense support in other libraries. In this context, client suspense means it takes advantage of <Suspense /> boundaries for data fetching and SSR suspense means that it supports React 18 SSR features (i.e. streaming HTML via renderToPipeableStream with <Suspense /> boundaries)

Library Client suspense SSR suspense Reference Notes
urql https://formidable.com/open-source/urql/docs/advanced/server-side-rendering/#using-react-ssr-prepass react-ssr-prepass is currently advised, but also works with renderToPipeableStream. Demo
GQty ⚠️ https://gqty.dev/docs/react/fetching-data#suspense-example Advises react-ssr-prepass for a 2-pass rendering approach
Relay https://relay.dev/docs/guided-tour/rendering/loading-states/ This is the only vetted approach so far by the React team.
React Query 🚫 https://tanstack.com/query/v4/docs/guides/suspense
SWR 🚫 https://swr.vercel.app/docs/suspense
micro-graphql-react ❓ (unclear from docs) https://arackaf.github.io/micro-graphql-react/docs/#react-suspense
Next.js Supports Suspense and React Server Components in Next 13

References

@wintercounter
Copy link

wintercounter commented Oct 25, 2022

For Hydration how about useQuery giving back an object that users need to add to DOM as serialized, namespaced data attribute. They would only need something like:

{
  'data-apollo-[queryId]': ...
}
const { data, hydrationProps } = useQuery(...)

return <div {...hydrationProps}>...</div>

Yes, this would be a manual work, but users can make sure it'll exist when it's needed, at the right place.


Another option would be custom script tags used similar way (hydrationTags instead of hydrationProps), the user needs to render it.

It'd work the same way Google Analytics's push. You have an array before the scripts are loaded, you can put it in . When DOM loads, it populates this array. Then push of this array is being monkey patched once the scripts are loaded, but it basically saves the query results in this array even before Apollo/React is loaded. It doesn't need to be an array or it doesn't need the patch either, it's just how Analytics works, but we can use the same logic.

One big benefit compared to data attributes that we don't need to rely on DOM at all, if the user renders the script tag we Apollo gives them, the data will be there in time.

@jerelmiller
Copy link
Member Author

jerelmiller commented Oct 25, 2022

@wintercounter good ideas! I think we'll probably lean more toward your 2nd idea there. We'd like to avoid manual stuff as much as possible to eliminate the possibility of error (i.e. forgetting to spread hydrationProps onto the div). This would also add a lot of overhead if a developer decides to enable suspense support globally, which would require a manual patch to every useQuery in your app (could be hundreds in a large app!) This discussion seems to allude to the idea of being able to write script tags during the stream, so I definitely want to explore this more.

While we don't yet have it flushed out, my guess is that we'll probably end up with some kind of function that you'll import server-side that will hook into renderToPipeableStream that will handle this for you. We are looking at how libraries like Relay and Next.js are handling this to get some inspiration.

@wintercounter
Copy link

wintercounter commented Oct 25, 2022 via email

@JoviDeCroock
Copy link
Contributor

Hey all, just notifying about a discrepancy here in the comparison table, urql supports both client and ssr suspense for over a year now. The reason behind us advising react-ssr-prepass is because Suspense on the server hasn't worked for a long time and we wanted a way to make it work out of the box. When leveraging streamed react you can use the ssr-exchange as a cache and convey the data between server and client easily.

I have put together a small example here where if you omit the script it streams everything in correctly but with the script still seems to be having a react warning which I will look at soon 😅

@jerelmiller
Copy link
Member Author

@JoviDeCroock Thanks for the info! I've updated the table above to reflect this. I was looking at the docs for info and all I could find on suspense was the section about SSR. Appreciate the confirmation that it works and the demo you put together!

@laverdet
Copy link
Contributor

The component will not render beyond the useQuery call in this case

I don't think this is what you want to do. Two different invocations to useQuery in the same component shouldn't waterfall, they should be dispatched at the same time. For example:

const query1 = useQuery(...);
const query2 = useQuery(...);
return <div>{query1.field.result}{query2.field.result}</div>;

Then the return value is something like const result = { data { get [fieldName]() { /* throw */ } } } which throws if the field is not resolved. This also ties in nicely with @defer, which would behave in a similar same way.

@jerelmiller
Copy link
Member Author

jerelmiller commented Oct 27, 2022

@laverdet thats a really good callout. You're right, the proposal would not allow you to parallelize queries in the same component. I do agree your proposed behavior makes sense and that we should allow it. This seems to fall in line with an earlier suggestion. Thanks for the callout!

Edit: I've added a section in the proposal above to call this behavior out explicitly and removed the statement about not rendering beyond the useQuery call.

@wintercounter
Copy link

wintercounter commented Oct 27, 2022 via email

@laverdet
Copy link
Contributor

laverdet commented Oct 27, 2022

Regarding the opt-in process I also might recommend just making a new hook entirely. Suspense is such a radical departure from the existing model that I think trying to implement it with a toggle is a fool's errand. Also, from the consumer side the two models are completely reversed. Without suspense the component has to say "what do I look like when I am loading", but with suspense the consumer may or may not even get a chance to answer that question.

About SSR and hydration I agree that sending down normalized cache [cache.extract() -> cache.restore()] is a fine approach but not the best approach. The way I'm doing this now is similar to the method described in the RFC. You send down the query results in a side-channel and write them to the local Apollo cache via cache.writeQuery. While hydrating, if the client receives a query that it can answer from the cache then it will respond with that data. If it cannot answer the request then it will suspend until either the side-channel is closed or until it can complete the request. Since markup must be sent before side-channel data [see "Injecting Into the SSR Stream"] this will be a common case. By suspending, React will continue to render the plain markup until Apollo can resolve the suspense. There is also the chance that the user changes the application state while the page is still hydrating and causes a new query request which will be never be answered by the SSR side-channel. In that case you might want to go to the network immediately, but detecting the condition is tricky [definitely possible though].

The side-channel technique is described under "Injecting Into the SSR Stream" here: reactwg/react-18#114

Building the aforementioned side-channel is definitely out of the scope of Apollo but familiarity with the technique is good to have. I think that a sound implementation on the Apollo side would naturally support both the cache.restore(cache.extract()) technique and the incremental cache.writeQuery technique. One approach would be a callback on ApolloClient options like beforeNetworkDispatch(query, variables): Promise<void>. This would allow Apollo to ask the consumer if it wants to wait on side-channel information for that query. If we are expecting side-channel information we'd avoid resolving the promise until we can call cache.writeQuery. After writing the query we'd resolve the promise, Apollo would check the cache and see that it can answer the query without going to the network.

@twavv
Copy link

twavv commented Oct 28, 2022

Found this as I was wondering if/when Apollo would be ready for use with NextJS 13. Exciting things ahead it seems!

As a best practice, we should avoid request waterfalls as much as possible. There may be cases where you want to run multiple useQuery hooks in the same component. We should allow this use case and only suspend when a developer tries to access properties on data.

I'm not sure if this is a best practice, but I wonder if this is too magic. Not all data is accessed synchronously during render. I can imaging something like

const WaitlistButton = () => {
  const userInfo = useQuery(...);
  return <button onClick={() => {
    fetch(`/api/waitlist?user-id=${userInfo.data.userId}`, { ... })
  }>Join the waitlist</button>
/>

(i.e., where the data isn't accessed until a callback is triggered). I don't believe Suspense works when a promise is thrown during an event handler.

I wonder if instead we can do opt-in batching (like the ReactDOM.unstable_batchedUpdates API)? I do wonder how often people use multiple queries in a single component -- seems like a big point of GraphQL to begin with is to reduce number of roundtrips.

Looking forward to this!!!!!

@laverdet
Copy link
Contributor

@travigd great point and something I hadn't considered because this access pattern isn't a part of my workflow, but it is a totally valid one.

I wonder if it makes sense to return a promise for suspense queries, and for @defer directives. There is a preliminary RFC for a new use hook (it's literally just called use) which would be responsible for unwrapping promises.
https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md

@twavv
Copy link

twavv commented Oct 31, 2022

use seems promising! Also seems like it would work with batching if you did

const promiseOne = apolloClient.query(...);
const promiseTwo = ...;

const dataOne = use(promiseOne);
const dataTwo = use(promiseTwo);

A bit verbose but not the end of the world.

@adamesque
Copy link

I'm wondering if there are enough edge cases around suspending for queries to use this as an opportunity to make an intentional shift toward useFragment as the primary API for Apollo Client's React integration, so that components can observe the specific fragments they care about and the framework can manage throwing promises if those fragments haven't yet been (or are in the process of being) fetched.

With this approach, we might decide not to have useQuery suspend at all, and possibly promote something like the proposed useBackgroundQuery as the primary suspense entrypoint.

@jerelmiller
Copy link
Member Author

@laverdet @travigd these are really great points!

That use proposal has a lot of really interesting points. I suspect Apollo client will use (pun intented) that use hook at some point in a future version.


A few interesting things I noted in that use proposal:

This bit of code first stuck out to me:

function Note({id, shouldIncludeAuthor}) {
  const note = use(fetchNote(id));

  let byline = null;
  if (shouldIncludeAuthor) {
    const author = use(fetchNoteAuthor(note.authorId));
    byline = <h2>{author.displayName}</h2>;
  }

  return (
    <div>
      <h1>{note.title}</h1>
      {byline}
      <section>{note.body}</section>
    </div>
  );
}

Particularly, this line of code:

const author = use(fetchNoteAuthor(note.authorId));

This implies that promises are unwrapped before rendering can continue since one would assume that note.authorId would likely be resolved from data in the fetchNote call. I wonder if this would suffer from the same kind of request waterfall that this proposal originally started with (as pointed out by @laverdet here).

In fact, later in the proposal, this is stated:

If a promise passed to use hasn't finished loading, use suspends the component's execution by throwing an exception. When the promise finally resolves, React will replay the component's render. During this subsequent attempt, the use call will return the fulfilled value of the promise.

I understand this to mean that the component would suspend immediately for pending promises. Given this proposal from the React team (understanding this could change between now and release), I wonder if we should do the same (which would revert this proposal to an earlier version).

@travigd you make a great point about accessing the data in event handlers. My guess is this kind of data access happens more often than multiple useQuery calls in a single component. It does bring up an interesting question: if the data returned by useQuery is only used in event handlers, does it make sense that we should block rendering by suspending the component? Perhaps this should be up to the developer to selectively disable suspense in these cases rather than Apollo trying to be overly smart about the intention.

Relay seems to solve this use case with a completely different hook called useLazyLoadQuery (COMPLETELY different kind of execution than the way a useLazyQuery in Apollo works). I suspect we can avoid the added API footprint in Apollo by simply a suspense: false option to the hook.

@laverdet you do bring up an interesting point about introducing a completely new hook and something I've also considered (perhaps I should call out in this proposal). A new hook could afford us some advantages such as:

  • We can introduce a new lifecycle to the hook specifically for suspense thats easily distinguishable from useQuery
  • We can avoid the need to support deprecated options such as partialRefetch
  • We could limit options such as the fetchPolicy to only support policies that make sense in the context of a suspense query (i.e. you can't use a cache-only fetch policy)
  • We could expand the set of options independently of useQuery
  • We could move away from some of the internal complexity of the useQuery implementation

This does come at the cost of a larger API footprint, but might be worth the tradeoff. With the addition of the upcoming useFragment and useBackroundQuery, there is the chance we introduce too much decision fatigue as each "query" hook has its own set of tradeoffs. Introducing a new hook would put our total "query" hooks count up to 4 (useQuery, useLazyQuery, useBackgroundQuery, and useSuspenseQuery (or whatever name we choose)).

This also comes at the cost of allowing suspense to be globally enabled (<ApolloProvider suspense={true} /> doesn't make sense if using suspense means using the new hook). Perhaps a new hook might reduce the demand for this option naturally anyways as this would mean either existing apps migrate to the new hook over time, or new apps use the new hook out of the gate.

Some other interesting points:


in the current version of React, an unstable mechanism allows arbitrary functions to suspend during render by throwing a promise. We will be removing this in future releases in favor of use. This means only Hooks will be allowed to suspend.

This is interesting. While it make sense this would be a hook-only API, I'd be curious how the React team plans to enforce linting rules and compiler optimizations for libraries like Apollo that would consider using use under the hood. I think this emphasizes the point that we need to take caution in how we communicate suspense to our users. We'd like to avoid breaking changes to Apollo if/when we migrate from our upcoming implementation to use.


Similar to async functions in JavaScript, the runtime for use maintains an internal state machine to suspend and resume; but from the perspective of the component author, it looks and feels like a sequential function:

I really like this point and something we are striving for with our implementation of a suspense query. We want it to feel like a sequential function, despite the async nature of queries.

@laverdet
Copy link
Contributor

laverdet commented Nov 1, 2022

About API footprint I think making it a separate hook would actually reduce the footprint. The API is not the sum of all exported functions but is actually the sum of the possible options permutations of those functions. Adding a radical new option to useQuery isn't a marginal increase in complexity; it doubles the complexity since now there are twice as many possible behavior patterns useQuery could exhibit. Like you pointed out, there's several possible combinations of options that just don't even make sense in the suspense world so you could eliminate them entirely from the new function signature. I also don't think a globally-enabled suspense is even a compelling or advisable feature due to how different the behavior is. Especially if the React team ends up moving forward with use.

About waterfalls with use I'm not sure where we'll end up there, and their RFC doesn't make that clear. I think we'd probably see something like: const author = fetchNoteAuthor(); const content = fetchNoteContent(); return <div><span>{use(author)}</span><p>{use(content)}</p>;. Clearly the framework would need to eagerly catch the promises in this case to avoid uncaughtRejection failures.

@adamesque
Copy link

@travigd you make a great point about accessing the data in event handlers. My guess is this kind of data access happens more often than multiple useQuery calls in a single component. It does bring up an interesting question: if the data returned by useQuery is only used in event handlers, does it make sense that we should block rendering by suspending the component? Perhaps this should be up to the developer to selectively disable suspense in these cases rather than Apollo trying to be overly smart about the intention.

I think it definitely makes sense to block rendering for this case because the event handler depends on that data to function correctly — what should happen if the user clicks and the data hasn't returned yet? To bind the handler to the rendered DOM node, the data must be present. If you truly don't want the query to block rendering, useLazyQuery seems like the natural path to take.

I'll ask again — aren't useFragment and useBackgroundQuery the correct primitives for a render-as-you-fetch React/Suspense architecture? Given that any query might soon contain a @defer'd fragment, trying to manage that via a normal useQuery hook that supported Suspense sounds like a nightmare.

@jerelmiller
Copy link
Member Author

jerelmiller commented Nov 1, 2022

The API is not the sum of all exported functions but is actually the sum of the possible options permutations of those functions

Thats a really good point @laverdet. suspense: true at a glance seems like such a small change, but you're right in that in combination with everything else, it drastically increases the complexity. The more we talk about this, the more I'm inclined to think a new hook might be the way to go.

I also don't think a globally-enabled suspense is even a compelling or advisable feature due to how different the behavior is

Generally I agree with you, but I think there is a use case for this if we do end up embracing the suspense: true option in useQuery. If you're starting a new app, or are a relatively new app, with the potential to grow over time and want to go all-in on suspense, the global option makes it easier to make suspense your default, rather than remembering or needing to add suspense: true to every one of your queries. I don't think already large apps would use this feature as I believe they would instead migrate over time.

Again, I think this global option is moot if we introduce a new hook instead. Opting into suspense would be just using the new hook, so the global option doesn't make sense in this case.

@jerelmiller
Copy link
Member Author

jerelmiller commented Nov 1, 2022

I'll ask again — aren't useFragment and useBackgroundQuery the correct primitives for a render-as-you-fetch React/Suspense architecture? Given that any query might soon contain a @defer'd fragment, trying to manage that via a normal useQuery hook that supported Suspense sounds like a nightmare.

@alessbell do you have any thoughts on this? You've got a bit more expertise in useBackgroundQuery and @defer than I do.

@jpvajda jpvajda added this to the Release 3.8 milestone Nov 1, 2022
@jerelmiller
Copy link
Member Author

jerelmiller commented Nov 1, 2022

@adamesque I'll post a couple thoughts about your question (@alessbell feel free to correct me where I'm wrong).

aren't useFragment and useBackgroundQuery the correct primitives for a render-as-you-fetch React/Suspense architecture?

I'm not sure useBackgroundQuery is going to help much for this pattern. From my understanding, the query is going to kick off as soon as the component mounts, much like useQuery. useBackgroundQuery will be a bit more of a render optimization where you can make sure your component doesn't oversubscribe to data changing in the cache that it doesn't care about which can help you avoid some rerenders. The prime use case for this is lists. You might not want a parent component to rerender every time data from a single item in the list is updated in the cache. A child component could however subscribe to changes in the cache for that item via useFragment.

client.query + the cache might be all we need for the render-as-you-fetch pattern. We'll do some more testing on this as we implement suspense, but I believe that because client.query lets you load data and prime the cache, a suspense query shouldn't have to suspend since that data can just be read from the cache.

As for @defer, I think we still need to figure out how a @defer'd query is going to interact with suspense. I've added this as an outstanding question in the RFC.

@jpvajda
Copy link
Contributor

jpvajda commented Nov 1, 2022

@adamesque Thanks for all your feedback here. We appreciate it! I just had a few clarifying questions.

  • Do you have a use case in mind where you'd be using Suspense with @defer That you can share?
  • Do you have a use case where you'd using @defer with SSR, or React Server Components That you can share?

The Apollo Client team has been thinking about @defer in a more client side query type of way, where a client developer would set @defer on fragments they want to de prioritize the loading of on the client side. With the inclusion of Suspense, SSR and now React Server Components we are trying to imagine how @defer would fit into those approaches, or would it be a stand alone approach that you could use when necessary?

@laverdet
Copy link
Contributor

laverdet commented Nov 2, 2022

We'd want to use Suspense everywhere, and @defer is also a big win, so the answer to when we'd use them together is "basically everywhere". Maybe you have a query like query Query { getMessages { id content author @defer { ...AuthorDetails } } } in case you want to display message content immediately and let message authors pop in later. There are tons of field resolvers which are non-critical and deferring would be a great win for getting content on the screen asap.

const ByLine = forMessage => <div>{forMessage.author.name}</div>;

const Message = message => {
    return <div>
        <Suspense fallback={<Loading />}><ByLine forMessage={message} /></Suspense>
        <p>{message.content}</p>
    </div>;
};

const MessageList = () => {
    const { getMessages } = useSuspenseQuery(GetMessages);
    return <ul>{
        getMessages.map(message => <li key={message.id}><Message message={message} /></li>)
    }</ul>;
}

I'd expect MessageList to suspend to its ancestor suspense boundary until the message content comes in. Then the message content will render and ByLine will suspend until the author information is ready.

I would think a sound implementation would work the same way during SSR. I think that some users will just opt to omit @defer directives in SSR but React is theoretically capable of streaming the rendered content in the same way it would be suspended in an SPA. See: reactwg/react-18#37. I think the tricky part would be how to stream the raw query results through the client side-channel in order to inject it into the local client cache. In an ideal world we'd have the ability to stream multi-part query results straight into InMemoryCache but I think it's totally acceptable to punt on that and let the client suspend (i.e. show non-interactive HTML) until the entire query is complete on the server side and delivered to the client.

@jpvajda
Copy link
Contributor

jpvajda commented Nov 2, 2022

@laverdet Thank you for the excellent example here! Super helpful. 🙏 Just one clarifying question in this example, this component wouldn't be a React Server Component, this would be a client side component?

@adamesque
Copy link

@jpvajda and @jerelmiller I appreciate the thoughtful comments & questions!

  1. We don't yet use @defer, but it feels natural to me that since @defer operates over fragments, I might want to consume that fragment declaratively in a component via useFragment, and for that component to suspend until the deferred data is available/in cache. That feels more natural/predictable than attempting to suspend, for instance, when a deferred field is accessed on the returned data since that access might not happen during render (might happen in an event handler, etc).
  2. I don't yet have a use case for @defer with SSR or RSC.

So we don't use @defer today, but we are pursuing a heavily fragment-based approach which is informing my POV here. In fact, until useFragment is out of preview stage, we're writing a lot of our own tooling around fragment->query tracking, refetching on fragment read misses, etc.

@laverdet The potential problem I see in your example is that the ByLine component code doesn't show any indication it might suspend because we're not declaring any sort of resource usage — only accessing fields on what might be a POJO, especially since the object is passed in via a prop. At least for me, the Suspense docs that show a resource prop passed in with a resource.messages.read() call is an important signifier and one that can be statically analyzed to discourage reading a resource from an event handler, etc.

I might prefer to rewrite your example as

// This component suspends
const ByLine = ({authorId}) => {
  // sugar wrapping the useFragment API for this component
  // (the contract is cumbersome for an example)
  const {data: name} = useByLineFragment(authorId);
  return <div>{name}</div>;
};

// This component doesn't suspend
const Message = message => {
  return <div>
    <Suspense fallback={<Loading />}><ByLine authorId={message.author.id} /></Suspense>
  </div>;
};

// This component suspends
const MessageList = () => {
  const {data: messages} = useSuspenseQuery(GetMessages);
  return <ul>{
    messages.map(message => <li key={message.id}><Message message={message} /></li>)
  }</ul>;
};

I also think you might want to express MessageList more like this:

const App = () => {
  useBackgroundQuery(GetMessages);
  return <MessageList />;
}

const MessageList = () => {
  const { data: messages } = useMessageListFragment();
  return <ul>{
    messages.map(message => <li key={message.id}><Message message={message} /></li>)
  }</ul>;
}

@SiarheiLazakovich
Copy link

Hi! It would be useful to have loading state in case when suspensePolicy set to initial. Because in this case on refetch we do not fall in Suspense#fallback.

@jerelmiller
Copy link
Member Author

@SiarheiLazakovich I've been thinking something similar would be nice in case you want to show some kind of UI indication that a fetch is happening. I'm considering exporting the networkStatus as a means to track it.

I'd like to avoid the loading flag if at all possible because I'd like to discourage anyone using useSuspenseQuery to have to manage loading states since that is what suspense is for.
I'll keep you updated on how the API shapes up in this regard. Thanks for the feedback!

@gaearon
Copy link

gaearon commented Mar 24, 2023

What's the state of SSR support in the current implementation? I suppose it might be tricky if you don't "own" the stream (e.g. like Next.js owning it), and we don't have a built-in API for transferring the data for hydration on the client. If it's not supported yet, it would be good if the Hook could throw in the SSR environment to avoid people relying on it accidentally. Otherwise (if the data isn't being serialized to the client), it would be very fragile because it would attempt fetching from both environments.

@wintercounter
Copy link

One solution would be if the hook would give the user a component to be rendered which would render the necessary data inline. It's a manual way of doing it, but I think I could live with that. Another option is the "old-school" query component. That actual can render anything necessary.

@jerelmiller
Copy link
Member Author

jerelmiller commented Mar 24, 2023

@gaearon we are still in the early stages of the SSR story as we've been really focused on ensuring it works as intended with all the features of Suspense itself (i.e. with useDeferredValue and startTransition).

We don't yet have a solution for streaming cache updates to the client, so you're correct, its likely very fragile right now since it likely fetches in both environments (I say likely because I'm still experimenting with SSR on my own and don't have a definitive answer on the current behavior). My current understanding of the problem is written up in the "How do we hydrate the client cache with data fetched server-side?" section in the original post.

My thinking was that we wouldn't own the stream, but rather write to it in some way. I have a lingering sense that there is probably something I've completely missed with the mental model of SSR and how its intended to work with a client like ours, so feedback here would be helpful if you have any.

I will definitely consider throwing on the server, or at the very least, making this a bit more "opt in" to avoid communicating that we've got this fully figured out. Appreciate the feedback there.

@gaearon
Copy link

gaearon commented Mar 24, 2023

My current understanding of the problem is written up in the "How do we hydrate the client cache with data fetched server-side?" section in the original post.

Yeah that looks right to me. The problem right now is we don't offer any way to inject data into the stream, and reactwg/react-18#114 suggests writing your own Transform Stream, which (I think) you can experiment with — but isn't possible if the underlying stream isn't exposed (such as with a framework). I might be wrong though. But this seems like the first thing to try.

@jerelmiller
Copy link
Member Author

suggests writing your own Transform Stream

This seemed like the right first step, so appreciate the confirmation that this is a good thing to try.

isn't possible if the underlying stream isn't exposed

This was also my worry. My thinking is that we can at least start with support for users that have access to the stream (such as those that have their own SSR implementation) and we get in touch with frameworks (or you/the React team) to figure out how to approach this in the framework space. At least this problem isn't unique to Apollo Client, but we can at least contribute the conversation where helpful.

@pkellner
Copy link
Contributor

@gaearon asked a few comments ago "what is the state of SSR implementation". I don't mean to interrupt this awesome discussion, but just wanted to make sure others understand that working with Apollo can be done through Next.js server components just like any other external service (though limited I'm sure on what can be done). I wrote a simple helper method that I call in my RSC's that does a POST to the GraphQL server and then returns the data to apollo client for processing.

That is, the helper function is this: https://gist.github.com/pkellner/cf1154a7bfb1873253702b11c8df5fb1

and then call it like this in my RSC's.

import React from "react";
import { commonGlobalDataQuery } from "../../gql/common/auth";

import { getDataFromGql } from "@/lib/getDataFromGql";
import { initializeApollo } from "@/lib/apolloClient";
import {getCodeCampYear} from "@/lib/utils";

initializeApollo();

async function getData() {
  return await getDataFromGql(commonGlobalDataQuery,{},"commonGlobalData");
}

export default async function HomeHeader() {
  //const authInfoValue: AuthInfo = useContext(AuthInfoContext);

  const {
    data: { codeCampYears: codeCampYears, configDataList: configDataList },
  } = await getData();
  ...

My initializeApollo code is similar to what is in Next.js examples here: https://github.com/vercel/next.js/tree/89fffa694dc238eb40bd52809c8cbcbc39c2ec8b/examples/api-routes-apollo-server-and-client

@laverdet
Copy link
Contributor

We're using a custom browser and server link which transmits query results over a transform stream. This is basically what is proposed in the RFC so I just want to confirm that the results are very impressive. The custom link approach is really nice because it gracefully handles @defer queries as well.

@jerelmiller
Copy link
Member Author

@laverdet that's really good to hear! I was hoping we could create a custom link to handle this, so I'm really glad to hear this is an approach that has worked.

Do you pass in the stream provided from renderToPipeableStream as argument to that link, or how do you typically hook this up?


@pkellner thanks for posting that! Might be a decent stop-gap until we have a more "official" solution there.

@pkellner
Copy link
Contributor

@laverdet , this does depend on the alpha of nextjs 13 though. I'm hoping it releases for production soon.

@laverdet
Copy link
Contributor

Yes, basically. There's an extra layer of indirection because we publish non-Apollo information over the transport stream as well, so it's abstracted away into what we call the side-channel. It's all integrated into our frontend code so sharing is a bit difficult. There's also a good deal of "opinion" baked in. For example, we have some tricks in place to ensure that the HTML markup is sent before the JSON query result which isn't super obvious.

This is what the server link looks like. We use a schema link so the query operations are being dispatched directly in the frontend service. The browser link accepts the side channel payloads and injects the query events into the link as it receives them. Obviously this isn't super useful without the other underlying implementations but the approach is sound.

server.ts

import { ApolloLink } from "@apollo/client/link/core/ApolloLink.js";
import { SchemaLink } from "@apollo/client/link/schema/index.js";
import { Observable } from "@apollo/client/utilities/observables/Observable.js";
// [...]

export async function makeServerTransportLink(pageContext: ServerPageContext) {
	const context = contextFromParams({ ... });
	const link = new SchemaLink({ context, schema });
	return makeServerSendLink(pageContext).concat(link);
}

function makeServerSendLink(context: ServerPageContext) {
	return new ApolloLink((operation, forward) => {
		const id = context.apolloQueryId++;
		const key = makeOperationKey(operation.query, operation.variables);
		context.publishSideChannelPayload(GQL_QUERY, null, [ id, key ]);
		const observable = forward(operation);
		return new Observable(observer => {
			const subscription = observable.subscribe(
				result => {
					context.publishSideChannelPayload(GQL_NEXT, key, [ id, result.data ]);
					observer.next(result);
				},
				error => {
					context.publishSideChannelPayload(GQL_ERROR, key, [ id, error ]);
					observer.error(error);
				},
				() => {
					context.publishSideChannelPayload(GQL_DONE, key, id);
					observer.complete();
				});
			return () => subscription.unsubscribe();
		});
	});
}

@jerelmiller
Copy link
Member Author

Awesome. Thanks for sharing @laverdet!

@jerelmiller
Copy link
Member Author

Hey all 👋 We've got an exciting update to share!

We've released an experimental package with support for Apollo Client with Next 13 and React Server Components. Check out the announcement blog post here: https://www.apollographql.com/blog/announcement/frontend/using-apollo-client-with-next-js-13-releasing-an-official-library-to-support-the-app-router/

This is our first step toward greater SSR and RSC integration with React. If you'd like to provide feedback or understand more of the interals, @phryneas has created an RFC in that repo for public view/comment. We'd love any and all feedback you may have!

Next.js was a logical first step for us as most of the activity and questions around SSR support came from those using Next.js. We aren't finished and plan to bring this integration to more frameworks and solutions.


As an update on the Suspense front, we are nearing completion on our Suspense functionality. We'll be releasing a beta v3.8 version in the next couple weeks that delivers on our Suspense story. Stay tuned for more information as we we get ready for this stage. If you haven't tried out our alpha and would like to provide feedback and/or report bugs, please do so by creating an issue in this repository. Thanks!

@alessbell
Copy link
Contributor

Hi everyone, a quick update on two new hooks for fetching/reading data with Suspense: useBackgroundQuery_experimental and useReadQuery_experimental are available as of 3.8.0-alpha.14 🎉

API documentation and a general overview are available here.

What do these hooks do? To quote the docs:

useBackgroundQuery_experimental allows a user to initiate a query outside of the component that will read and render the data with useReadQuery_experimental, which triggers the nearest Suspense boundary above the component calling useReadQuery_experimental while the query is pending. This means a query can be initiated higher up in the React render tree and your application can begin requesting data before the component that has a dependency on that data begins rendering.

We're pretty excited about them. As always, you can try them out via npm i @apollo/client@alpha and we'd love any feedback about them. Feel free to drop comments on this issue here or open a new issue with any improvements/bugs ❤️

@jerelmiller
Copy link
Member Author

Hey all 👋

Its been a few and I wanted to provide an update on progress. For those unaware, we recently started shipping beta versions of our next minor release v3.8. This is a huge milestone as this means we consider v3.8 feature complete 🎉.

A few highlights from the betas that I wanted to call out related to Suspense:

  • All v3.8.0-beta.x versions of @apollo/client have dropped the _experimental suffix on the newly introduced hooks (this includes useSuspenseQuery, useBackgroundQuery, and useReadQuery). These hooks are now imported without the _experimental suffix.
import {
  useSuspenseQuery,
  useBackgroundQuery,
  useReadQuery,
} from '@apollo/client';

This was an important change as it signifies that we consider these hooks stable.

  • v3.8.0-beta.4 shipped support for the skip option in useSuspenseQuery and useBackgroundQuery. I know many of you have been waiting on this option before more fully adopting these new hooks.

We hope to start shipping a release candidate for v3.8 in the next week or two. We'd appreciate any and all feedback and/or bug reports to ensure stability in this release. It has certainly shaped up to be one of our biggest minor releases to date! For those interested in our progress, feel free to follow our v3.8 milestone.

It's been a long journey, but we can't wait to get this into the hands of everyone! Thanks to all of you that have tried out the alphas/betas and provided feedback and bug reports. It has been tremendously helpful!

@jerelmiller
Copy link
Member Author

Hey all 👋

Another exciting update to share! We released our first v3.8 release candidate version today 🎉! You can try this release out with the rc tag:

npm install @apollo/client@rc

As such, we've frozen changes to our v3.7.x version in anticipation of a stable release of v3.8 in the next couple weeks.


I'll highlight a tweak we've made to the setup process in the release candidate version that I believe streamlines the Suspense feature a bit more.

In previous prerelease versions, you'd need to create a SuspenseCache instance in order to use the new useSuspenseQuery or useBackgroundQuery/useReadQuery hooks. That looked like this:

import { ApolloClient, ApolloProvider, SuspenseCache } from '@apollo/client';

const suspenseCache = new SuspenseCache();
const client = new ApolloClient({ ... });

// ...

<ApolloProvider client={client} suspenseCache={suspenseCache} />

With the release candidate, we now transparently create the Suspense Cache behind-the-scenes the first time you use a Suspense-related feature. Now all you need is to import one of the new hooks and start using it!

If you're using an alpha or beta prerelease, you can migrate to this new release candidate by deleting the SuspenseCache import and instantiation.

-import { SuspenseCache } from '@apollo/client';

-const suspenseCache = new SuspenseCache();

-<ApolloProvider client={client} suspenseCache={suspenseCache} />;
+<ApolloProvider client={client} />;

We've provided a deprecation error to make this transition obvious. As a result, the SuspenseCache import will be removed in the stable release of v3.8.0.


As of this release candidate, we have worked through all known Suspense-related issues and completed all the work planned for our next minor release. We will now be furiously writing and finishing up documentation for all features in this release. Unless we get newly reported bugs for this release candidate, this version will essentially become the v3.8 stable version.

If you'd like to help us out, please try this version out and let us know if you find any bugs! We can't wait to get this out to you all 🎉. Thanks!

@XavierLeTohic
Copy link

I encountered a weird issue with useSuspenseQuery using fetchPolicy: "no-cache" with 3.8.0-rc.1:

I had two components, identical in the implementation, calling two different queries, both using no-cache, one was rendering on a route perfectly well with SSR, the other one was making the server defaulting to client-side rendering as shown on this error:

Screenshot 2023-07-27 at 17 31 01

Took a long time to try to figure out the difference between the two before I found out that the GraphQL server was taking less than 150ms to resolve the query of the working component, while on the other hand the other query was taking more than 1.5s for the GraphQL endpoint to send the response.

It seems that at some point there is a timeout and it fallbacks to client-side rendering.

Is that something we can prevent?

@phryneas
Copy link
Member

@XavierLeTohic This sounds like you are making that request on the client and server simultaneously, which kinda defeats the purpose of SSR.
Are you using the https://github.com/apollographql/apollo-client-nextjs package? That should prevent that.

@jerelmiller
Copy link
Member Author

@XavierLeTohic would you mind opening a separate issue for this? I'd like to keep this issue related to the suspense implementation + updates. Doing so would help us track this more effectively. Thanks!

@jerelmiller jerelmiller removed this from the Release 3.8 milestone Jul 28, 2023
@jerelmiller
Copy link
Member Author

We're proud to announce the public release of Apollo Client 3.8 with Suspense integration 🎉!! Thanks so much to all of you that tried out the prereleases and provided feedback!

For more information on this release, including the other features we released, please see the announcement post.

Since we are now public with this release, I'm going to go ahead and close out this issue. We will continue to iterate on our Suspense integration in future versions so please keep an eye out for additional announcements. Feel free to open new issues for bug reports and reach out to us for any feedback you may have. Thanks!

@alessbell alessbell unpinned this issue Aug 9, 2023
@github-actions
Copy link
Contributor

github-actions bot commented Sep 7, 2023

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
For general questions, we recommend using StackOverflow or our discord server.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests