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

How to get a promise from the useQuery hook ? #5268

Closed
gregorybellencontre opened this issue Sep 3, 2019 · 33 comments
Closed

How to get a promise from the useQuery hook ? #5268

gregorybellencontre opened this issue Sep 3, 2019 · 33 comments

Comments

@gregorybellencontre
Copy link

gregorybellencontre commented Sep 3, 2019

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

@chaunceyau
Copy link
Contributor

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
    />
  )

}

@gregorybellencontre
Copy link
Author

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

@gregorybellencontre
Copy link
Author

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.

@chaunceyau
Copy link
Contributor

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

@gregorybellencontre
Copy link
Author

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.

@gregorybellencontre
Copy link
Author

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 :)

@caderitter
Copy link

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

@vamshi9666
Copy link

@caderitter onCompleetd is available in useLazyQuery ?

@alexhawkins
Copy link

alexhawkins commented Mar 10, 2020

@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>
  );
}

@lifeiscontent
Copy link
Contributor

lifeiscontent commented Apr 7, 2020

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

@webdeb
Copy link

webdeb commented Apr 14, 2020

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())

@webdeb
Copy link

webdeb commented Apr 14, 2020

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.

@jakobo
Copy link

jakobo commented Apr 27, 2020

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
    />
  );
}

@tyankatsu0105
Copy link

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

@gregorybellencontre
Copy link
Author

Thank you all for these great solutions !

@emersonhsieh
Copy link

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

@Zikoel
Copy link

Zikoel commented Nov 23, 2020

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.

@martinezguillaume
Copy link

martinezguillaume commented Dec 22, 2020

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

@pseuyi
Copy link

pseuyi commented Jan 8, 2021

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}
    />
  )
};

@doflo-dfa
Copy link

doflo-dfa commented Oct 29, 2021

This kind of feels dirty but I am not sure it really violates any rules and works as a reliable way to get a promise from the query ?

const _getRowCompletionPromiseRef = useRef<Promise<{ rowId: string }>>()
const _getRowCompletionRef = useRef<({ (row?: { rowId: string }): void })>()
const [_getRow, _rowData] = useLazyQuery<{ rowId: string }>(projects.GET_ROW, {
    onCompleted: (d => {
        if (_getRowCompletionRef.current && d) {
            _getRowCompletionRef.current(d)
        }
    }),
    onError:(()=>{
        if (_getRowCompletionRef.current) {
            _getRowCompletionRef.current()
        }
    })
})
getRow(rowId: string) {
    /**
     * I am not sure you should do this in react 
     * but technically I don't think I have broken any rules
     */
    _getRowCompletionPromiseRef.current = new Promise((complete, reject) => {
        _getRowCompletionRef.current = (d) => {
            if (d !== undefined) {
                complete(d)
            }
            else {
                reject()
            }
        }
        _getRow({
            variables: {
                teamId: projectContext.teamId, contextId: state.boardId,
                rowId: rowId
            }
        })
    })
    return _getRowCompletionPromiseRef.current
},

@luatnd
Copy link

luatnd commented Oct 31, 2021

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
    />
  );
}

How can I get error.extension.code of this query style?

@doflo-dfa
Copy link

@luatnd, will the client direct method above interact with the cache or does the user have to make sure they connect this new apollo client to the same cache (ie inMemoryCache ) used elsewhere by the hooks.

@narayanpromax
Copy link

I saw network tab and it seems to use Cache as defined in Apollo Config

@ricardo-rp
Copy link

I am honestly very confused as to why useLazyQuery's handler doesn't return a promise. It leads to weird patterns

@dqtkien
Copy link

dqtkien commented Nov 29, 2021

I am honestly very confused as to why useLazyQuery's handler doesn't return a promise. It leads to weird patterns

In Ubuntu, useLazyQuery return Promise but not in Mac, don't know why we have this magic here

@martinezguillaume
Copy link

martinezguillaume commented Nov 29, 2021

I am honestly very confused as to why useLazyQuery's handler doesn't return a promise. It leads to weird patterns

In Ubuntu, useLazyQuery return Promise but not in Mac, don't know why we have this magic here

What ? That doesn’t make any sense

@andreataglia
Copy link

As a Vue3 user I've created this little composable function in order to have a promisified version of the apollo APIs.

import { ApolloQueryResult, OperationVariables } from '@apollo/client';
import { useQuery } from '@vue/apollo-composable';
import { DocumentParameter, VariablesParameter } from '@vue/apollo-composable/dist/useQuery';
import { ref } from 'vue';


// promisification of the apollo query
export function useApolloQuery<TResult = any, TVariables extends OperationVariables = OperationVariables>(document: DocumentParameter<TResult, TVariables>, variables: () => VariablesParameter<TVariables>) {
  const enableLoginQuery = ref(false);
  const { onResult, loading: queryLoading, onError } = useQuery(
    document, 
    variables,
    () => ({
      enabled: enableLoginQuery.value,
      notifyOnNetworkStatusChange: false
    }));

  const apolloQuery = () => new Promise<ApolloQueryResult<TResult>>((resolve, reject) => {
    console.log('calling query apollo');
    enableLoginQuery.value = true;
    onResult(res => {
      if (!res.loading) {
        enableLoginQuery.value = false;
        
        if (!res.data) {
          reject('no data');
        }
        resolve(res);
      }
    });
    onError(err => {
      enableLoginQuery.value = false;
      console.error(err)
      reject(err);
    });
  });

  return {
    apolloQuery,
    queryLoading
  };
}

On a sample query you would use it like this:

import {useApolloQuery} from '... wherever that is';

const { apolloQuery } = useApolloQuery(UserQueryDoc, () => ({
  username: form.value.username,
  otherVars: 'other sample var'
}));
const res = await apolloQuery();

All fully typed. I use graphql-tools to generate the types.

@brainkim
Copy link
Contributor

brainkim commented Nov 29, 2021

In Ubuntu, useLazyQuery return Promise but not in Mac, don't know why we have this magic here

@kevindangvn I am mildly certain that this has to do with stale node_modules dependencies. If not, I may need to take another day off.

@hwillson
Copy link
Member

hwillson commented Jan 4, 2022

useLazyQuery now returns a promise with the result, which should address this issue.

@hwillson hwillson closed this as completed Jan 4, 2022
@jackykwandesign
Copy link

useLazyQuery now returns a promise with the result, which should address this issue.

thanks lord, async/ await Promise is the way how we make our product, not some weird pattern rely on callback
I have been using this wrap for a while, but giveaway the cache ability

import { DocumentNode, OperationVariables, useApolloClient } from '@apollo/client';
import { useCallback } from 'react';

  
export function useCustomLazyQuery<TData = any, TVariables = OperationVariables>(query: DocumentNode) {
    const client = useApolloClient();
    return useCallback(
        (variables: TVariables) =>
            client.query<TData, TVariables>({
                query: query,
                variables: variables,
                fetchPolicy:'network-only',
            }),
        [client]
    );
}

@xorinzor
Copy link

@hwillson useLazyQuery still seems to work differently from useMutation, it's not clear to me how I'd get a promise from it

@ezeikel
Copy link

ezeikel commented Jul 27, 2022

@hwillson @xorinzor Yeah, I can't see how to get the Promise from the hook either and the documentation makes no reference to a Promise - https://www.apollographql.com/docs/react/data/queries/#manual-execution-with-uselazyquery

@ezeikel
Copy link

ezeikel commented Jul 27, 2022

Actually, ignore me. The function that the hook returns, returns a Promise when called. it's just not stated in the docs.

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

No branches or pull requests