How to get a lazy suspense query?

Hello! Please help a newcomer to Apollo.

I’m using apollo-client in a React app. I’m using the latest RC with the hot new useSuspenseQuery. My issue is that I would like to have a combination of a “suspense query” with a “lazy query”: i.e. I would like to have a query that is not executed automatically on render, instead waiting for the code to trigger it explicitly (because I want to run it conditionally, based on the props). And I would like the query to suspend my component when it does get triggered, until the results are ready. Is that possible?

I’ve noticed that useSuspenseQuery() has a skip option that is perhaps related to what I want, but I can’t find much docs on it or examples on its intended use.

Thank you!

Hey @andreimatei :wave:

This is a great question! We do have a plan to add a hook for this pattern in 3.9. Unfortunately we couldn’t quite get it ready for the upcoming 3.8 release, so we left it out. I have a particular vested interest in this one because I think this particular hook would be super useful for preloading data.

When this hook lands, it will likely work a bit more like useBackgroundQuery in the sense that it will return a queryRef which can then be passed to useReadQuery to suspend and read the data. Keep an eye on our roadmap which we’ll keep up-to-date with this info.

As for now, your best bet is the skip option as you mentioned. Our docs are still in flux as we haven’t released it yet, so it makes sense you’re unable to find anything there. We will be releasing docs for these new features as soon as we release 3.8. If you want a sneak preview, check out this docs preview of the Suspense page. We have an open PR that documents the skip behavior if you’d like to read through that and understand how it works.

Let me know if those preview docs are enough to answer your question, otherwise feel free to followup here and I’d be happy to answer any more questions you have!

Thanks for your answer, @jerelmiller !
The skipToken option worked for me.

Let me ask you something related - do you see suspense as useful for queries that are to be run in response to user events (e.g. a button being clicked), as opposed to queries that are run by renders? Today, I imagine people use useLazyQuery for such use cases, where the query’s variables depend on which particular button was clicked, and then wait for the query’s promise to resolve (or use the query’s onCompleted callback) before updating some state based on the query’s response. Will any of the suspense hooks (for example, the future one that might be coming in 3.9) be useful in these scenarios? Or is “suspending” only something that makes sense in renders, and not in event handlers?

Since we’re talking, I also have some confusion about useLazyQuery - this might betray me being new to react/frontend development in general. I’m confused about why useLazyQuery is a hook. Being a hook, I’m forced to call it in every render, and I have to plumb its result (the query function) to other functions that I call from my render. Also, if I’m not mistaking, calling the query function returned by useLazyQuery also causes a re-render.
I have a use case where I think that what I want is a lighter-weight way to call a GraphQL query: when a certain user action happens, I want to call a query to load more data, and I don’t need this call to trigger a re-render (because I’ll trigger the re-render by updating some useState state). So, I don’t feel the need to declare my query at the top level of my render, and I wonder why I’m being burdened to do it. Is there a way to call queries without using hooks - something more akin in spirit to a fetch call?

The skipToken option worked for me.

Glad to hear it! To be honest, I think you’re the first one to use this new feature, so congrats :tada: :laughing:

do you see suspense as useful for queries that are to be run in response to user events (e.g. a button being clicked)

Absolutely! In fact, Relay’s useQueryLoader/usePreloadedQuery hooks work this way. The hook that we will release in 3.9 will resemble this a little bit.

The case I think it will be super nice for is preloading a part of your UI. For example, imagine hovering over a button that opens a modal which displays some data. If you can start loading that data on hover vs waiting for the modal to render, the modal is going to appear much snappier because you may be able to load the data faster than the user clicks to open it. If the user does happen to open the modal before the query is finished, Suspense will take over and you’ll see the loading state instead. This pattern seems like a natural fit for this type of API.

I’m sure others will come up with great ideas and patterns for it. Its certainly a much more streamlined API than having to skip a query and pair it with a useState for these types of preloading scenarios.

I’d recommend taking a read of this excellent Relay tutorial that describes some of the benefits of this pattern. Again, our hook will look a bit different than this one, but the concepts will be similar.

We’ll have much more to share in the coming weeks as we start working on this new hook and start our 3.9 prerelease series (alphas, betas, etc).

Or is “suspending” only something that makes sense in renders, and not in event handlers?

Think of Suspense more like a “not ready” state for your component, where that “not ready” state is determined during a render cycle. Its not so much that the event handler itself starts suspending the component, its that state updates caused by that event handler will cause the component to Suspend on the next render. Does that difference make sense?

Also, if I’m not mistaking, calling the query function returned by useLazyQuery also causes a re-render.

This is correct, though the reason for this is due to the value of the 2nd item in the tuple returned by useLazyQuery.

const [loadQuery, { data, loading, error }] = useLazyQuery()

Those data/loading/error states need to reflect the state of the most recent call to the query function. In this way, useLazyQuery is managing the query state for you. You can almost think of this hook as useQuery with skip set to true until you call the query function for the hook. Unless you absolutely have a need to useState for something extra, I’d recommend just using the data property returned by this hook instead of calling your own set function. This will reduce a render for you.

One other important point to make here is that useLazyQuery will respond to cache updates. This is a point a lot of people tend to miss with this hook. If you use your own state setter for to store data from this hook, data changing in response to cache updates are going to be missed, which means your UI might get out of sync/outdated, especially if you run mutations that alter the data returned from this query.

Its not against the rules to store your own state using data returned from the query function, but I’d recommend avoiding it and just using data if you’re not doing anything special with it.

I have a use case where I think that what I want is a lighter-weight way to call a GraphQL query: when a certain user action happens, I want to call a query to load more data, and I don’t need this call to trigger a re-render

Funny enough, people tend to forget you can just use the client directly to make queries (myself included). There is nothing that says you have to use the hooks to query data. You’ll just get the additional benefit that those hooks will ensure your component rerenders when cache updates are made to data in the query. The hooks will also manage the loading state for you.

If you don’t care about cache updates, I’d just call client.query({ query }) directly:

import client from './path/to/client';

function MyComponent() {
  const [data, setData] = useState(null);

  return (
    <button
      onClick={async () => {
        const { data } = await client.query({ query });
        setData(data);
      }}
    />
  );
}

Apollo also returns a useApolloClient hook if you prefer to get the client that was passed into ApolloProvider

import { useApolloClient } from '@apollo/client';

function MyComponent() {
  const client = useApolloClient();

  // ...
}

Again, the key difference here is that you’ll miss cache updates by using this directly instead of the hooks. Certainly a tradeoff you can make a decision on for your app.

Hope this helps! Please let me know if you have any additional questions.

This is all very useful, thank you!

Those data /loading /error states need to reflect the state of the most recent call to the query function. In this way, useLazyQuery is managing the query state for you.

Got it. But this all means that I can’t have the same useLazyQuery be used by multiple, independent event handlers that want to call the query with different variables, right? For example, let’s imagine I have a list of elements, and each element has a button to expand it. Clicking the button wants to run a query and pass the element’s ID as a query variable. It’s conceivable that the user even clicks multiple buttons in close succession, while previous queries are running. In such a case, should I be using the client.Query directly, as you taught me, instead of useLazyQuery?

Or is “suspending” only something that makes sense in renders, and not in event handlers?

Think of Suspense more like a “not ready” state for your component, where that “not ready” state is determined during a render cycle. Its not so much that the event handler itself starts suspending the component, its that state updates caused by that event handler will cause the component to Suspend on the next render. Does that difference make sense?

So you’re saying that the way this works mechanically is that the event handler’s use of the suspense query immediately causes a rerender, which in turn suspends the component, right? That makes sense; I hadn’t quite tied things together.


You talk about caching a bunch, which brings me to a question I’ve had ever since I saw the useQuery hook. There’s something fundamental that’s not clear to me and I haven’t seen it explained in the docs.
Let’s say I do this:

const {loading, error, data: collectionData} = useQuery(GET_COLLECTION);

On the first render, I have loading set and no data. Then, the query completes and I get a second render that gets data. Then, let’s say a third render happens for some reason unrelated to the query. Now, when useQuery runs, obviously we don’t expect the query to actually make a network call; and we expect to get data. So the question is - where does data come from? Does it come from the cache (so it’s subject to the cache perhaps expiring?), or is the latest data returned by a query guaranteed to exist through some other mechanism, unrelated to the cache?