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