Apollo-client: How to get a promise from the useQuery hook ?

Created on 3 Sep 2019  路  19Comments  路  Source: apollographql/apollo-client

Hello
I am trying to use the useQuery hook with react-select : AsyncSelect, and I need a promise to load options asynchronously.
Is it possible to get a promise with this hook ?
The only way I found is to trigger a refetch and use the promise, but it's not really good because it doesn't use the cache...

Most helpful comment

For folks coming across this later who need async/await on useLazyQuery you can just use the apollo client directly with the useApolloClient hook. The client's query method returns a promise per the docs. You can then save whatever is needed from the query via react's useState hooks.

import { useApolloClient } from "@apollo/client";
import react, {useState} from "react";

const LOAD_OPTIONS = gql`
  query($searchTerm: String!) {
    options(title: $searchTerm) {
      id
      title
    }
  }
`;

function OptionsInput () {
  const client = useApolloClient();
  const [results, setResults] = useState([]);

  return (
    <input
      placeholder="Search for option"
      onChange={async (e) => {
        const { data } = await client.query({
          query: LOAD_OPTIONS,
          variables: { searchTerm: e.target.value },
        });
        setResults(data.options);
      }}
      results={results} // find a way to render results
    />
  );
}

All 19 comments

If by options, you mean an input similar to the react-select: AsyncSelect documentation.. perhaps useLazyQuery would fit your use case perfect.

useLazyQuery doesn't execute when the component is first rendered as it does with useQuery. useLazyQuery provides a function to execute the query a later time.

Input with Search

const LOAD_OPTIONS = gql`
  query ($searchTerm: String!) {
    options (title: $searchTerm) {
      id
      title
    }
  }
`

function OptionsInput() {
  const [ loadOptions, { loading, error, data } ] = useLazyQuery(LOAD_OPTIONS)

  return (
    <input
      placeholder='Search for option'
      onChange={e => loadOptions({ variables: { searchTerm: e.target.value } }) }
      results={data} // find a way to render results
    />
  )

}

Thanks for this, it seems a lot better. I'll try it :)

I just made a try and it doesn't help for my case actually.

The AsyncSelect component is waiting for a promise in the loadOptions prop.
When I make a change in the input, this function is triggered and it takes the query data at that time. But it's not accurate because it's the previous data, as request didn't finished running.

It seems I can't wait for the request to end before returning the promise to the AsyncSelect component.

My current solution is to use the default Select component and handle the async manually.

I through together an example on CodeSandbox, take a look and let me know if it fits your use case:

https://codesandbox.io/s/tender-aryabhata-n55q7?fontsize=14

Yes, that's what I was saying in my comment, I did this finally.
But it doesn't use the AsyncSelect component but the default Select component.

Actually, the AsyncSelect has the loading part built-in, and avoid to write some asynchronous related logic.
But it's using a Promise, and Apollo useQuery and useLazyQuery do not send back a Promise.
So I can't wait data from the query, before passing it to AsyncSelect
For now, I made it with the classic Select component, and it's fine. But can be improved :)

You could probably make this work using the onCompleted option callback that you can provide to useQuery, which fires when the query is complete.

@caderitter onCompleetd is available in useLazyQuery ?

@caderitter onCompleetd is available in useLazyQuery ?

Yes, it works. Here is an example.

function getProductData(data: any) {
  return get(data?.viewer?.user, 'products.edges')
}

export default function BuildYourOwnPage() {
  const [ products, setProducts] = useState([]);

  const [
    getProducts,
    { called, data, loading, error },
  ] = useLazyQuery(PRODUCT_CATALOG_QUERY, {
    variables: {
      category: 'wine',
    },
    fetchPolicy: 'network-only',
    onCompleted: (d) => setProducts(getProductData(d))
  });


  useEffect(() => {
    getProducts();
  }, []);

  return (
      <Container>
        <BreadCrumbs />
        <BuildYourOwnTopNavContainer >
          <BuildYourOwnHeaderTextContainer>
            <BuildYourOwnHeaderText>Build your own 6-Pack</BuildYourOwnHeaderText>
            <ProductResultsCount>{products.length} Results</ProductResultsCount>
          </BuildYourOwnHeaderTextContainer>
          <CategoryTabs getProducts={getProducts}/>
          <Spacer/>
        </BuildYourOwnTopNavContainer>
        {called && loading && <Loading />}
        {!loading && get(data?.viewer?.user, 'products') &&
          <ProductCatalog products={products}/>}
    </Container>
  );
}

@hwillson adding the same API interface that useMutation has to useLazyQuery might be a nice win for 3.0

Just came across that issue, and just wanted to add my 2莽. it would be really handy to get a promise from the lazyquery call.

You know it just feels right to be able to handle the response in the context of the query
loadData().then(data => console.log())

onCompleted can not solve different scenarios if you have for examples two functions, which are calling the query

const callOne = () => loadData().then(// do this)
const callTwo = () => loadData().then(// do that)

It makes sense to use the same query, but you probably want to execute different tasks in every scenario.

For folks coming across this later who need async/await on useLazyQuery you can just use the apollo client directly with the useApolloClient hook. The client's query method returns a promise per the docs. You can then save whatever is needed from the query via react's useState hooks.

import { useApolloClient } from "@apollo/client";
import react, {useState} from "react";

const LOAD_OPTIONS = gql`
  query($searchTerm: String!) {
    options(title: $searchTerm) {
      id
      title
    }
  }
`;

function OptionsInput () {
  const client = useApolloClient();
  const [results, setResults] = useState([]);

  return (
    <input
      placeholder="Search for option"
      onChange={async (e) => {
        const { data } = await client.query({
          query: LOAD_OPTIONS,
          variables: { searchTerm: e.target.value },
        });
        setResults(data.options);
      }}
      results={results} // find a way to render results
    />
  );
}

@Jakobo
Your solution is good for me!!
Thanks for the sharing your knowledge :)

But I don't know why useLazyQuery does not support async await.

@hwillson
Is there plan for supporting async await at useLazyQuery?

Thank you all for these great solutions !

For those using apollo-boost, I had to import useApolloClient from @apollo/react-hooks for Jakobo's solution to work. Otherwise, great work!

Any news on that? The solution from @Jakobo can be useful but break a little the code base cleaning and to use the correct function provided by Apollo is more correct in my opinion! Why not provide a returned promise? This is also perfectly compatible with the actual functionality and all scenarios are covered without workaround, more of this Mutation lazy query already provide this feature.

You can follow this

For some reason, refetch function return a promise, so the thing is to use const query = useQuery({ skip: true }) (enable skip to not call it immediately) and after you can call const result = await query.refetch()

Why refetch returns a promise and lazy query doesn't ? Mystery

in case anyone is looking, here is what is looks like using the useApolloClient hook with AsyncSelect:

import React from 'react';
import AsyncSelect from 'react-select/AsyncSelect';

const AsyncSelectInput = () => {
  const fetchOptions = async () => {
    const { data } = await client.query({
      query: QUERY
    });

    return data ? data.map(d => ({value: d, label: d})) : [];
  }

  return (
    <AsyncSelect
      cacheOptions
      defaultOptions
      loadOptions={fetchOptions}
    />
  )
};
Was this page helpful?
0 / 5 - 0 ratings