Apollo-feature-requests: react-apollo useQuery: add support for Suspense

Created on 8 Aug 2019  路  33Comments  路  Source: apollographql/apollo-feature-requests

Sorry if this has already been discussed but I couldn't find any feature request for supporting React Suspense api.

Although Suspense isn't officially ready for data fetching, some libraries already take advantage of this feature (react-hooks-fetch, react-apollo-hooks, react-i18next, built-in React.lazy...).

Why

Data loading is quite painful to handle at the time, especially when you have multiple queries.

const { data: data1, loading: loading1 } = useQuery(query1);
const { data: data2, loading: loading2 } = useQuery(query2);
if (loading 1 || loading2 || ...) {
  // here goes some repetitive code
}

Code handling data loading is also very repetitive.

Solution

Similar to what react-apollo-hooks did, what about adding a boolean suspend experimental option (which defaults to false) that would make useQuery throw the query promise to trigger the Suspense api?

const { data } = useQuery(query1, { suspend: true });
// if we reach this line, data is populated and safe to use

This would certainly comes with some caveats like the fact it would only work with a cache-first fetch policy, but most of the time benefits of triggering Suspense are greater than caveats:

  • code becomes cleaner and simpler by allowing to write it as if everything is synchronous
  • repetitive code to handle loading can be reduced to the usage of a <Suspense> component

Question is: what is the roadmap about Suspense support ? Would it be possible to have an experimental implementation in a near future? or wait for the Suspense api to be officially ready for data fetching?

Thanks

project-apollo-client

Most helpful comment

Apollo Client 3 will have React Suspense support. The level of this support is still a bit up in the air, as we don't want to tie the release of AC 3 to React's release schedule, and Suspense for data fetching is still experimental. At a minimum though, all Suspense functionality in the latest release channel (when AC 3 is published) will be supported.

All 33 comments

I think this is a very important feature to have. I wonder why it hasn't attracted that much attention yet... I also came from react-apollo-hooks and relies on its suspense feature a lot :)

Agree. Actually I think it's pretty easy to implement it yourself by wrapping useQuery. But it would be really nice to have it as an experimental (opt-in) option to the hook!

@jfrolich do you mind sharing your implementation on useQuery wrapper that supports suspense?

I don't use suspense for useQuery yet. But it's as simple as throwing a promise when data is not in the cache and needs to be fetched. Now that I think of it, I don't think the useQuery exposes a promise so you might need to do more than just wrapping useQuery.

It's a bit more of just throwing the promise, since the current useQuery rely on useRef which is dropped when something is thrown. I had to make a few changes to the original code and might have some leaks/bugs.

I removed everything that is linked to lazy queries, it was easier to work on it like this.

Taken from https://github.com/apollographql/react-apollo/blob/master/packages/hooks/src/utils/useBaseQuery.ts

import {useQuery as useApolloQuery} from '@apollo/react-hooks';
import {getApolloContext} from '@apollo/react-common';
import {QueryData} from '@apollo/react-hooks/lib/data/QueryData';
import {useDeepMemo} from '@apollo/react-hooks/lib/utils/useDeepMemo';
import {useContext, useEffect, useReducer} from 'react';

+ // we need to store all queries references somewhere
+ // since the refs are dropped when throwing the promise
+ // this lives in the module for now but could be stored in the context, I guess
+ const queryDataRefs = new Map();
const forceUpdateRefs = new Map();

export default function useBaseQuery(query, options) {
+   // Oops, conditional hook, but let's use the real useQuery if suspense is not needed
+   /* eslint-disable */
+   if (options && !options.suspend) {
+       return useApolloQuery(query, options);
+   }
+   /* eslint-enable */

    const context = useContext(getApolloContext());
-   const [tick, forceUpdate] = useReducer(x => x + 1);
+   const [tick, forceUpdate] = useState(0);
    const updatedOptions = options
        ? {...options, query}
        : {query};

-   const queryDataRef = useRef();
+   // we used the serialized options to keep track of the queries
+   const queryKey = JSON.stringify(updatedOptions);
+
+   if (!queryDataRefs.has(queryKey)) {
+       queryDataRefs.set(
+           queryKey,
+           new QueryData({
+               options: updatedOptions,
+               context,
+               forceUpdate: () => {
+                   const snapshotOfForceUpdate = Array.from(
+                       forceUpdateRefs.values(),
+                   );
+
+                   snapshotOfForceUpdate.forEach((fn) => {
+                       forceUpdateRefs.delete(fn);
+                       fn(x => x + 1);
+                   });
+               },
+           }),
+       );
+   }

-   const queryData = queryDataRef.current;
+   const queryData = queryDataRefs.get(queryKey);
    queryData.setOptions(updatedOptions);
    queryData.context = context;

+   // I had a hard time with this one
+   // if you have several parent-children components that triggers the same query,
+   // we want to keep track of all the hooks we have to forceUpdate
+   if (!forceUpdateRefs.has(forceUpdate)) {
+       forceUpdateRefs.set(forceUpdate, forceUpdate);
+   }

    // `onError` and `onCompleted` callback functions will not always have a
    // stable identity, so we'll exclude them from the memoization key to
    // prevent `afterExecute` from being triggered un-necessarily.
    const memo = {
        options: Object.assign(Object.assign({}, updatedOptions), {
            onError: undefined,
            onCompleted: undefined,
        }),
        context,
        tick,
    };

    const result = useDeepMemo(() => queryData.execute(), memo);

    const queryResult = result;

    useEffect(() => queryData.afterExecute({lazy: false}), [
        queryResult.loading,
        queryResult.networkStatus,
        queryResult.error,
        queryResult.data,
    ]);

    useEffect(
        () => () => {
            queryData.cleanup();

+           // I have to admit, I don't really know for that one
+           // but better play it safe
+           queryDataRefs.delete(queryKey);
+           forceUpdateRefs.delete(forceUpdate);
        },
        [],
    );

+   if (options && options.suspend && result.loading) {
+       forceUpdateRefs.delete(forceUpdate);
+       throw result.observable.result();
+   }

    return result;
}

If anyone is willing to update this piece of code, make it easier or more robust, I'll be grateful. :)

EDIT: I updated the code, we had some trouble with it. It should work better this way.

React released Concurrent Mode (Experimental) with suspense
primarily aimed at early adopters, library authors, and curious people

So since we are both curious people and library authors this might be the best time to explore how we can use suspense with apollo 馃殌

https://reactjs.org/docs/concurrent-mode-suspense.html#for-library-authors

Glad to see this issue already exists and thanks for the amazing work on Apollo! I'm almost completely out of my element here, but wanted to mention: perhaps it would be interesting to infer a @defer directive whenever a Suspense Boundary is encountered?

I'm on pins and needles to see how the Apollo team integrates suspense! The benefits, like useTransition support while you wait for an Apollo-powered page to load, will be massive.

Apollo Client 3 will have React Suspense support. The level of this support is still a bit up in the air, as we don't want to tie the release of AC 3 to React's release schedule, and Suspense for data fetching is still experimental. At a minimum though, all Suspense functionality in the latest release channel (when AC 3 is published) will be supported.

Really appreciate looking at suspense for data fetching, we've been using react-apollo-hookss suspend: true in production for a couple of months and haven't noticed any issues.

Suspense and ErrorBoundarys impacts a lot of async handling code, so being able to introduce it to new Apollo users, or as part of the 3.0 upgrade process, could let people switch to hooks and the latest best practices in one step.

Thank you so much for mentioning that @rajington, I had no idea there was an experimental implementation.

I wonder what are strategies to handle enforced cache-first fetchPolicy? In several cases, we needed to use network-and-cache to keep the data up to date whenever a query is re-executed (page re-visited).

The only thing that comes to my mind is to have that query for a second time there with suspense disabled and that fetchPolicy to keep cache up-to-date. It feels weird but cannot think of a better solution.

@alflennik just FYI react-apollo-hooks was the unoffical hooks before Apollo's, so it's nonstandard in other ways as well, like you have to wrap it with another provider

@FredyC cache-only is interesting as well, hopefully it shouldn't suspend at all since it'sn't async

@rajington No my point is how to keep data up-to-date if we cannot use other than cache-first/only fetchPolicies.

What really intrigues me is how the fetch as you render approach will be accomplished. 馃く

What really intrigues me is how the fetch as you render approach will be accomplished. 馃く

I don't think that's actually a concern of the Apollo. Have a look at a really interesting discussion over at react-router

Anyone knows if there is already an experimental build of Apollo with suspense. I'd like to test it.

Apollo Client 3 will have React Suspense support. The level of this support is still a bit up in the air, as we don't want to tie the release of AC 3 to React's release schedule, and Suspense for data fetching is still experimental. At a minimum though, all Suspense functionality in the latest release channel (when AC 3 is published) will be supported.

@hwillson Does the above still hold true? I looked at v3 beta docs https://www.apollographql.com/docs/react/v3.0-beta/data/queries/ and it does not mention suspense.

Apollo Client's suspense work has been started, but we've had to put it on-hold for a bit due to time/resource constraints. Suspense support isn't going to make it into the AC 3.0 release unfortunately, but the good news is we've just hired more AC team members, one of which is going to start by getting suspense support in place right after AC 3 launches.

Right now our focus is on paying down a lot of tech debt in Apollo Client as we nail down the new cache structure and API. A lot of this has to be done to effectively support suspense, but more immediately it has to be done to help get rid of outstanding Apollo Client bugs. We're streamlining the internals to make them much more dependable and the changes we have coming will eradicate a large amount of the open issues we currently have.

Until Apollo will release official useSuspenseQuery hook I've created own hook to manage this and collaborate with Apollo Client, you can take it here: https://www.npmjs.com/package/@brainly/use-suspense-query
Just read README and enjoy!

Any update on when the support of Suspense will be available now that AC 3 was released ?

Our Suspense plans are on-hold right now due to the fact that React concurrent mode is still only available via an experimental build, and we're not sure when that's going to change. We've decided to invest our time in other areas right now, but will definitely be diving back into all of this when it looks like concurrent mode will be available in a stable release.

@hwillson I understand that it is experimental, but it is been experimental since forever and alternatives such as urql, swr and react-query all support it if I'm not mistaken. At this point is very unlikely that the api will change drastically in the near future.

Suspense has been supported by urql for about 4 months now (https://github.com/FormidableLabs/urql/tree/main/exchanges/suspense). I really think apollo has significally changed the GraphQL client game, but I'm sad they became that library that kept on changing API and package organization without being able to adapt to new react features. I remember that it took quite a while until hooks were supported in the official apollo react core. And seeing how long it takes (@brielov has a great point about the API probably not changing) doesn't really show the innovative spirit that once drove apollo.

It doesn't really matter, I've switched to urql already, but I still hope that apollo catches up again in terms of feature parity and speed, its been my reliable und trusted gql client for years; and they still offer tremendous libraries and toolsets that help devs work with graphQL.

Apollo Client 3 will have React Suspense support. The level of this support is still a bit up in the air, as we don't want to tie the release of AC 3 to React's release schedule, and Suspense for data fetching is still experimental. At a minimum though, all Suspense functionality in the latest release channel (when AC 3 is published) will be supported.

Is suspense now supported in the new Apollo Client 3?

Our Suspense plans are on-hold right now due to the fact that React concurrent mode is still only available via an experimental build, and we're not sure when that's going to change. We've decided to invest our time in other areas right now, but will definitely be diving back into all of this when it looks like concurrent mode will be available in a stable release.

Yes, concurrent mode is still experimental but Suspense is widely used in production with React.Lazy and ErrorBoundary paradigm : https://reactjs.org/docs/code-splitting.html#reactlazy
Could be also under experimental flag in apollo client for opt-in when needed.

Yes, concurrent mode is still experimental but Suspense is widely used in production with React.Lazy and ErrorBoundary paradigm : https://reactjs.org/docs/code-splitting.html#reactlazy
Could be also under experimental flag in apollo client for opt-in when needed.

Yes, Suspense for Code Splitting is in production and widely used. But Suspense for Data Fetching (the kind of Suspense that Apollo would be doing) is very much part of Concurrent Mode. Data fetching suspense proper is only available in react@experimental.

For projects starting from scratch or starting a major refactoring (e.g. adopting GraphQL), leveraging Suspense patterns significantly changes how components are written, the amount of boilerplate and 3rd-party libs, and even how other patterns like SSR are handled.

We're currently stuck on AC2 (react-apollo-hooks supports Suspense) since it'll be easier to migrate components AC2 to AC3 than from older async handling to Suspense.

I have no idea the complexity of implementing this now and what that means for other things AC has in store, and it's frustrating some React target dates for Suspense launch have been missed, but I feel there's an opportunity to leverage AC early adopters (that likely came here in search of the best data fetching implementation) to help prepare AC@experimental for when Suspense does launch.

I was trying to make it work with Suspense and came up with the solution below. It was just a quick test and I'm not sure if it can cause problems.

useSuspendableQuery.js

import { useQuery } from "@apollo/client"

import { suspend } from "lib/suspend"

export const useSuspendableQuery = (...args) => {
  const result = useQuery(...args)
  if (result.loading) {
    suspend(new Promise((resolve) => !result.loading && resolve())).read()
  }
  return result
}

suspend.js

export const suspend = (promise) => {
  let status = "pending"
  let response

  const suspender = promise.then(
    (res) => {
      status = "success"
      response = res
    },
    (err) => {
      status = "error"
      response = err
    }
  )

  const read = () => {
    switch (status) {
      case "pending":
        throw suspender
      case "error":
        throw response
      default:
        return response
    }
  }
  const result = { read }

  return result
}

Usage

const Parent = () => {
  return (
    <React.Suspense fallback="Loading...">
      <Child />
    </React.Suspense>
  )
}

const Child = () => {
  const { data } = useSuspendableQuery(MY_QUERY)
  return data.field
}

@defer and @sterem directives are mentioned at here

These directives allows to fetch data incrementally. And I want to wrap components corresponding to @derefer ed fragment with <Suspense> as the following:

const fragment = gql`
  fragment ProductDetail on Product {
    # selections to be fetched lazy
  }
`

const DeferredFragmentComponent = ({ fragmentId }) => {
  // It's similar to client.readFragment but useFragment can throw promise if the fragment is annotated as deferred and the data is not delivered 
  const { fragmentData } = useFragment({ id: fragmentId, fragment }); 
  return (...) // render fragmentData
} 

const QueryComponent = () => {
  const { queryData: { product } } = useQuery(gql`
    query MyQuery {
      product(id: "p100") {
        __typename
        id
        name
        ...ProductDetail @defer
      }
    }
    ${fragment}
  `)
  return (
    <>
      <span>{product.name}</span>
      <Suspense fallback={<Loading />}>
        <DeferredFragmentComponent fragmentId={`${product.__typename}:${product.id}`} />
      </Suspense>
    </>
  )
}

Is there anything you are planning about this?

I'm using Apollo with NextJS (with concurrent mode enabled obviously).

I tried @Fasosnql's @brainly/use-suspense-query and with it I get infinite re-rendering of a component that uses it.

If I try @rafbgarcia's useSuspendableQuery example above, it works with client-only rendering, but it gets stuck on the loading state when rehydrating after SSR with nextjs. Also I get Can't perform a React state update on a component that hasn't mounted yet. in that case.

It's a bit sad to see such conservative stance on suspense support from Apollo's maintainers TBH: it's supported by literally all other data fetching libraries by now...

I've been patiently waiting for over a year for this feature to get implemented so I can standardize the way I structure my code around suspense. Sorry to be that guy, but I think I'm done waiting for "that one" feature that haven't got enough attention from maintainers to deserve some love, even though every other library has at least opt-in support. Switching to urql after years of betting on Apollo's stack.

Just wanted to link this issue https://github.com/apollographql/apollo-client/issues/5870 because it seems related according to this comment from Dan Abramov. Maybe it can help to better understand what's involved in this issue

Was this page helpful?
0 / 5 - 0 ratings