What is best practice for structuring sub component queries?

Hi all. I am having a hard time finding any docs/discussion about this.

I have a dashboard for a user. This dashboard has multiple pages for that user’s information.

As a hypothetical example, one page has multiple tables whose data can be resolved from the User type OR be fetched individually using the User Id when the component is called. Right now, I have queries for each table with their own loading states, errors, etc.

Another quick point, by using nested resolvers, some resolve very very slow, so putting them all in the User type may slow down the client side considerably.

Here is the current setup:

Server side

extend type Query {
    getUser(id: ID!): User!
    getUserData1(id: ID!): UserData1
    getUserData2(id: ID!): UserData2
    getUserData3(id: ID!): UserData3
    getUserData4(id: ID!): UserData4
}

Client Side

// UserDash.tsx

const Dashboard = () => {
   const { data, loading, error} = useGetUserQuery({variables: {id}})
   
   //   ... loading and error logic

   if (data) {
      return <>
         <UserTable1 />
         <UserTable2 />
         <UserTable3 />
      </>
   }
}
// UserTable1.tsx

const Dashboard = () => {
   const { data, loading, error} = useGetUserData1Query({variables: {id}})
   
   //   ... loading and error logic for this table

   if (data) {
      return <Table>...data</ Table>
   }
}

You generally want to model your types after the data they represent, not “per view” as it looks like you’re trying to do.

All the fields on a User should be on that type, not split between 3 different types that represent different slices of a User.

If you’re having performance issues with this approach, I recommend first digging into the problematic resolvers and seeing what’s causing them to take so long.

Thanks Trevor!

I know this is hard to type out and explain without having a concrete example, but what would be a best practice for handling error messages/states for each table if I am combining these into one cohesive query? Same goes for loading states (albeit a bit simpler to conceptualize)

It’s still correct to have 3 separate queries for the tables, so you can handle loading and error states individually per component (which it appears you’re already doing).

What I’m suggesting is, you don’t want to have all of these UserDataN types, just a single User and a single getUser field.

Your table queries will all query the getUser top-level field, but ask for different parts of the user depending on the data they present. So it might look like:

query UserTableContactInfo($id: String!) {
  getUser(id: $id) {
    id
    name
    email
  }
}
query UserTableAddress($id: String!) {
  getUser(id: $id) {
    id
    address {
      number
      street
      zip
    }
  }
}
query UserTableBillingInfo($id: String!) {
  getUser(id: $id) {
    id
    paymentMethods {
      type
      accountNumber
    }
  }
}

ok ok, so follow up question to this… I originally had what you suggested, however, I ran into a separate issue (which I almost guarantee is something I am simply overlooking). My resolver will refetch the user for each separate query since the user is the root result of the query. Does this make sense?

extend type Query {
    getUser(id: ID!): User!
}

// using your examples here:

type User {
   id: ID!
   paymentMethods: [PaymentMethods!]!
   ... other data
}

const resolver = {
   Query: {
      getUser(_, { id }, {dataSources}) => dataSources.User.getById(id)
   }
}

That’s correct. Each component is making a separate request to your endpoint and triggering the getUser resolver each time. But, that was also the case in your other solution, right? You were calling a root level resolver per query, each of which had to fetch a user from a datasource.

Now if you have a datasource that can cache the user result, it’ll only fetch that user once and give you a couple cache hits in the subsequent queries. I think this is the bit you’re really looking for.

DataSources documentation in case it’s helpful:

That was my bad, maybe I should clarify on the initial ask.

So with the initial ask, I am fetching the data with a predetermined ID when needed (i.e. it’s not resolved as a nested field for the User). Like so:

(Also assume these data points are called at separate times)

Separate Queries with id arg: (only calls the initial getUser endpoint once

query UserTableContactInfo($id: String!) {
  getUser(id: $id) {
    id
    name
    email
  }
}

query UserTableAddress($id: String!) {
  getUserData1(id: $id) {
    address {
      number
      street
      zip
    }
  }
}

const resolver = {
   Query: {
      getUser: (_, { id }, context) => return blah.getUserById(id),
      getUserData1: (_, { id }, context) => return blah.getUserAddressInfo(id),
   }
}

vs. One query, but nested fields (By calling two queries with the same root resolver, I would be hitting the getUserById endpoint twice)

query UserTableContactInfo($id: String!) {
  getUser(id: $id) {
    id
    name
    email
  }
}

query UserTableAddress($id: String!) {
  getUser(id: $id) {
    address {
      number
      street
      zip
    }
  }
}

const resolver = {
   Query: {
      getUser: (_, { id }, context) => return blah.getUserById(id)
   },
   User: {
      address: ( { id }, __, context) => return blah.getUserAddressInfo(id)
   }
}