Skip to content

RFC: React 18 SSR + Suspense Support #10231

Closed

Description

@jerelmiller

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

Activity

  1. wintercounter commented on Oct 25, 2022

    @wintercounter

    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.

  2. jerelmiller commented on Oct 25, 2022

    @jerelmiller
    MemberAuthor

    @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.

  3. wintercounter commented on Oct 25, 2022

    @wintercounter
  4. JoviDeCroock commented on Oct 26, 2022

    @JoviDeCroock
    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 😅

  5. jerelmiller commented on Oct 26, 2022

    @jerelmiller
    MemberAuthor

    @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!

  6. laverdet commented on Oct 26, 2022

    @laverdet
    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.

  7. jerelmiller commented on Oct 27, 2022

    @jerelmiller
    MemberAuthor

    @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.

  8. wintercounter commented on Oct 27, 2022

    @wintercounter
  9. laverdet commented on Oct 27, 2022

    @laverdet
    Contributor

    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.

  10. twavv commented on Oct 28, 2022

    @twavv

    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!!!!!

  11. laverdet commented on Oct 29, 2022

    @laverdet
    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

  12. twavv commented on Oct 31, 2022

    @twavv

    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.

  13. adamesque commented on Oct 31, 2022

    @adamesque

    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.

  14. 49 remaining items

  15. jerelmiller commented on Mar 24, 2023

    @jerelmiller
    MemberAuthor

    Awesome. Thanks for sharing @laverdet!

  16. jerelmiller commented on May 11, 2023

    @jerelmiller
    MemberAuthor

    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!

  17. alessbell commented on May 17, 2023

    @alessbell
    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 ❤️

  18. jerelmiller commented on Jun 28, 2023

    @jerelmiller
    MemberAuthor

    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!

  19. jerelmiller commented on Jul 13, 2023

    @jerelmiller
    MemberAuthor

    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!

  20. XavierLeTohic commented on Jul 27, 2023

    @XavierLeTohic

    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?

  21. phryneas commented on Jul 27, 2023

    @phryneas
    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.

  22. jerelmiller commented on Jul 27, 2023

    @jerelmiller
    MemberAuthor

    @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!

  23. removed this from the Release 3.8 milestone on Jul 28, 2023
  24. jerelmiller commented on Aug 7, 2023

    @jerelmiller
    MemberAuthor

    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!

  25. unpinned this issue on Aug 9, 2023
  26. github-actions commented on Sep 7, 2023

    @github-actions
    Contributor

    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.

  27. locked as resolved and limited conversation to collaborators on Sep 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions