trojanowski / react-apollo-hooks

Use Apollo Client as React hooks
MIT License
2.41k stars 110 forks source link

Is it possible to call multiple queries with useQuery? #120

Closed midoru0121 closed 5 years ago

midoru0121 commented 5 years ago

Hi, In advance thank you great library.

In our product, there is the a page which needs data from 2 or 3 GraphQL queries and we want to call multiple queries with one useQuery().

export const Curriculums = ({ children }) => {

  const { data, error } = useQuery(QUERY_CURRICULUMS)

  /* We want to call useQuery as below 
  const { data, error } = useQuery([
     QUERY_CURRICULUMS, 
     QUERY_COURSES, 
     QUERY_LESSONS
     ]
  ) 
  */

  if (error) {
    throw error
  }

  return children(data)
}

Is it possible to do the same things as compose() method defined in react-apollo?

https://www.apollographql.com/docs/react/react-apollo-migration#compose-to-render-composition

Or should we call useQuery with each queries?

Thanks.

FezVrasta commented 5 years ago

compose just calls multiple HOCs together, but it doesn't anything else, in fact you have to map the result of the queries manually to avoid them from being overridden by the next query.

What you want to do is compose multiple useQuery into your own custom hook:

const queryMultiple = () => {
  const res1 = useQuery(...);
  const res2 = useQuery(...);
  return [res1, res2];
}
midoru0121 commented 5 years ago

@FezVrasta

Thank you for your quick response. I understand and we'll call useQuery as described :) I'll close the issue.

dannycochran commented 5 years ago

@FezVrasta I think I am noticing a similar issue, not sure if this is expected behavior. The top level of my app is wrapped in an <ApolloHooksProvider />.

I have two components in the tree that each call useQuery with distinct queries against completely different parts of the schema. The component which renders last seems to cancel the in flight /graphql request created by the first component's useQuery call. Is it expected that only one network request can be in flight at any given time from useQuery? If so, that feels like an unnecessary restriction.

MarioKrstevski commented 5 years ago

I have a simple solution that worked for me.

const { data: dataR, error: errorR, loading: landingR } = useQuery(GET_RESTAURANTS);
const { data, error, loading } = useQuery(GET_DAILY_MENU);

I am simply renaming the fields useQuery is giving me in the first call, so that they can live in the same scope, otherwise the second query will overwrite the first.

TSMMark commented 4 years ago

Because react does not allow you to have a variable number of hooks, I still think there is a usecase for ONE hook that performs multiple queries.

In our situation, we are paginating a table and preloading a variable number of next pages.

For example, if user visits page 1, we preload page 2 and 3 after the

First ideas of API would be something like this:

const { data, loading, error } = useQuery(LIST, { variables: { limit: 10, offset: 0 } })
useManyQueries([
  [LIST, { skip: !data, variables: { limit: 10, offset: 1 } }],
  [LIST, { skip: !data, variables: { limit: 10, offset: 2 } }],
])

In the above example I've hardcoded it which is achievable with a static number of hooks, however if numPreloadedPages becomes dynamic it's not possible as per my understanding.

Example:

const currentPage = 0
const limit = 10
const numPreloadedPages = someDynamicValue()
// Let's say numPreloadedPages => 4
const { data, loading, error } = useQuery(LIST, { variables: { limit, offset: currentPage * limit } })

const preloadPages = Array.from(new Array(numPreloadedPages)).map((_, i) => i + currentPage + 1)
// => [1, 2, 3, 4]

useManyQueries(preloadPages.map((page) => [
  LIST, { skip: !data, variables: { limit, offset: page * limit } }
]))

Should I open a new issue for this?

FezVrasta commented 4 years ago

Why can't you use the existing "skip" option to conditionally run the queries?

TSMMark commented 4 years ago

I don't see how that's related and if you read the code I posted I am using the skip option

FezVrasta commented 4 years ago

Because react does not allow you to have a variable number of hooks, I still think there is a usecase for ONE hook that performs multiple queries.

It's not needed, you can use skip to get what you want. That's my point.

Also, to be fair, your example is not that great, why can't you run a single query to get all your pages? You should just change the limit accordingly 🤷‍♂

TSMMark commented 4 years ago

Sorry maybe I'm not explaining the problem well enough.

you can use skip to get what you want

You're still talking about a static number of useQuery. Sure. Yeah you could write 100 useQuery into a component and skip them dynamically. Am I missing something or are you really saying that is an ideal API?

run a single query to get all your pages

This only works if you have 100% cache redirect coverage, or I think possibly using returnPartialData?, which is not always possible or ideal. To my understanding, the way apollo resolves queries from the cache is by looking up that query from the cache given the exact same operationName + variables.

So if you have previously queried { offset: 0, limit: 100 }, and now are querying the same operation with variables { offset: 0, limit: 10 }, the cache will be missed and the network will be hit.

I'm fairly sure about that but please correct me if that's not the case.

P.S. if you want to learn how to use the expression "to be fair" correctly this is a good start https://www.collinsdictionary.com/us/dictionary/english/to-be-fair

asotog commented 4 years ago

mm had same question i have 2 use queries in the same component fetching with different queries, but i see 2 http requests, is it possible to only make 1 request ? is there any configuration i'm missing ?

ivan-kleshnin commented 4 years ago

@asotog something like this:

let DataQuery = gql`
  query BlogData {
    me {
      id
      email
    }
    posts {
      id
      title
    }  
  }`

function Page() {
  let router = useRouter()

  let {data, error, loading} = useQuery(DataQuery)
  ...
  // use data.me
  // use data.posts
}

?!

TSMMark commented 4 years ago

@asotog you want to use the apollo link batch http https://www.apollographql.com/docs/link/links/batch-http/

asotog commented 4 years ago

@asotog something like this:

let DataQuery = gql`
  query BlogData {
    me {
      id
      email
    }
    posts {
      id
      title
    }  
  }`

function Page() {
  let router = useRouter()

  let {data, error, loading} = useQuery(DataQuery)
  ...
  // use data.me
  // use data.posts
}

?!

@ivan-kleshnin @TSMMark sorry i'm new to apollo and graphql in general I have something like this:

const { data: carouselData } = useFetchCarousel();
const { data: featureSectionData } = useFetchFeaturedSection();

each of these hooks using useQuery with different queries each, but i see in the network log, 2 requests going to the graphql separate, so my question was if there is any way apollo handles the 2 request at the same time, or i need to put the 2 gql queries in same useQuery, what approach you would take in my case ? thanks in advance

ivan-kleshnin commented 4 years ago

Two hooks will result in two HTTP requests because by using two hooks you're basically saying: "I want to apply different policies to two queries". Those policies can be, for example:

My code above (single useQuery, multiple queried objects) will result in a single HTTP request. Note that quering two objects at the same time will affect caching behavior. But most of the times after:

query {
  foo
  bar
}

the consequent

query {
  foo
  baz
}

will reuse the cache for foo and won't query for it. So the "default" approach should probably be the one with a single useQuery and multiple query objects, not the one with multiple useQuery. I'd use the second only when it's really necessary.

asotog commented 4 years ago

will try that thanks so much

TSMMark commented 4 years ago

@asotog I still think the batch-http link might help you more than you think. It introduces http request batching automatically globally for queries that run "at the same time" within a certain timeout window. Using it, you could keep your multiple useQuery separate as they are, and let the apollo link take care of batching them when it makes sense. I'll also note that you can have individual queries opt out of batching, if needed.

However if you insist not using batch-http apollo link, you should follow @ivan-kleshnin's advice. But I will add to it: you might want to consider breaking your one query (comprised of the two smaller queries) into logical query Fragments (see links below). This is similar to how Relay works — Each component may "export" a query Fragment, then a parent component that actually makes the query is responsible for merging the query fragments into one complete query, and to make the actual query to the server.

This may help you achieve separation of data concerns, where your component still has defined colocated data dependencies, but you are merging the queries at a higher level to optimize HTTP traffic.

https://graphql.org/learn/queries/#fragments https://www.apollographql.com/docs/react/v3.0-beta/data/fragments/

difex01 commented 4 years ago

Something that worked for me was using another component :

User_Data.js

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import QUERY from '../../../Queries';
import Car_Info from './Car_Info';

const GET_USER_QUERY = QUERY.User.GET_USER_QUERY;

const User_Data = (props) => {

    const userId = props.match.params.id;

    const { loading, error, data } = useQuery(GET_USER_QUERY, {
        variables: {
            id: userId
        }
    });

    if(loading) return `Loading...`;
    if(error) return `Error: ${error.message}`;

    return (
        <Car_Info type={data.getUser.car_type}/>
    );
};

export default User_Data;

Car_Info.js

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import QUERY from '../../../Queries';

const GET_CAR_QUERY = QUERY.Car.GET_CAR_QUERY;

const Car_Info = ({type}) => {

    const userId = props.match.params.id;

    const { loading, error, data } = useQuery(GET_CAR_QUERY, {
        variables: {
            type
        }
    });

    if(loading) return `Loading...`;
    if(error) return `Error: ${error.message}`;

    return (
        <div> Content, etc </div>

    );
};

export default Car_Info;

Hope this help

githubjosh commented 4 years ago

If it's helpful to anyone, here's a great resource with several approaches How to run multiple queries at once using GraphQL and Apollo Client

Here's one approach (from the article above):

image

Took me a lot of searching to find this.

ryderwishart commented 3 years ago

I have a simple solution that worked for me.

const { data: dataR, error: errorR, loading: landingR } = useQuery(GET_RESTAURANTS);
const { data, error, loading } = useQuery(GET_DAILY_MENU);

I am simply renaming the fields useQuery is giving me in the first call, so that they can live in the same scope, otherwise the second query will overwrite the first.

Another simple solution I found useful is to wait on destructuring the results until you can control the name you give each value.

const restaurantQueryResult = useQuery(GET_RESTAURANTS);
const menuQueryResult = useQuery(GET_DAILY_MENU);
const menuData = menuQueryResult.data
const restaurantData = restaurantQueryResult.data
...
ifndefdeadmau5 commented 3 years ago

@githubjosh I would disagree with assessment in that article which

While this works great with this example, it has two drawbacks:

  1. Since we’re re-using the same variables in both components, if the two queries don’t accept the same variables (or their type definitions), we can’t have multiple definitions for them. For example, one if the two queries is marking a variable as required and the second query doesn’t mark it as required, an error will be thrown in this case as the definitions are not matching.

This just doesn't make sense because we definitely can declare variables separately for each query that being combined

  1. It becomes messy easily when it grows and difficult to pass the data around

Question, which one sounds better, getting same data with 1 single query OR multiple query with multiple round trip without getting any benefits? and I don't understand how can it be messier than have multiple useQuery hooks that'd end up use renaming like

const { data: queryAData, loading: queryALoading, error:queryAError } = useQuery(...)
const { data: queryBData, loading: queryBLoading, error:queryBError } = useQuery(...)
Bankalstep commented 3 years ago

Hello,

Trying to do as follow :

 const {data: dataReviews, loading: loadingReviews, error: errorReviews} = useQuery(GetReviews, {
});
const {data: dataRating, loading: loadingRating, error: errorRating} = useQuery(GetAverage, {
});

doesn't work for me. Most of the time on of the two requests just fails. I don't know how is that possible since that's a solution that many people have proved it valid ? how come that doesn't work for me ?

This is the whole code :

const Reviews: FunctionComponent = () => {
    const {data: dataReviews, loading: loadingReviews, error: errorReviews} = useQuery(GetReviews, {
        ssr: false,
        variables: {
            offset: 0,
            limit: 3
        }
    });

    const {data: dataRating, loading: loadingRating, error: errorRating} = useQuery(GetAverage, {
        ssr: false
    });

    if (loadingRating && loadingReviews) {
        return <div className={`${styles.loader}`}/>;
    }

    const reviews = !loadingReviews && !errorReviews && dataReviews ? dataReviews.reviews[0].reviews : null;
    const rating = !loadingRating && !errorRating && dataRating ? dataRating.average[0] : null;

    return (
        <div className={`${styles.netreviews_review_rate_and_stars}`}>
            <div className={`${styles.reviews_list}`}>
                <ReviewsSideInfo rating={rating} stats={reviews.stats} filter={reviews} recommandation={reviews}/>
                <ReviewsContainer reviews={reviews}/>
            </div>
        </div>
    );
}
export default Reviews;

At the end I get my variable reviews equals nulland then the rendering fails. Any help would be greatly appreciated. Thanks.

irgherasim commented 3 years ago

Hello everyone, I have an use case for which I can't find any useful resources. I have to run a query that returns a list of values. For each value in the list I have to call a query to get extra details. This means I have to run a variable number of useQuery, without knowing the number of times.

Is this achievable with usequery or should the change happen on server side to be able to send the list of inputs as an array and get the agregated results?

Thanks in advance!

I need something like this : `const {data, loading} =useQuery(FIRST_QUERY) ;

const listOfValues = data?.values;

listOfValues. forEach(val => { const {data: newData, loading : newLoading} = useQuery(SECOND_QUERY, { skip:! listaOfValues, variables :{ sentValue: val } } }) ;`

seaweasel commented 1 year ago

For the sanity of future generations, give each useQuery call a "queryKey" config variable or you may end up with overlapping queries.

{ data: q1} = useQuery({queryKey:'q1', queryFn: () => /** resource 1 **/}) { data: q2} = useQuery({queryKey:'q2', queryFn: () => /** resource 2 **/})