Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Architecting for deeply nested UIs #192

Closed
HriBB opened this issue Sep 5, 2016 · 9 comments
Closed

Architecting for deeply nested UIs #192

HriBB opened this issue Sep 5, 2016 · 9 comments
Labels

Comments

@HriBB
Copy link

HriBB commented Sep 5, 2016

This is not an issue but a question about application architecture with react-apollo. Specifically with deeply nested UIs.

Let's say I have the following react-router routes defined

const salonRoutes = {
  path: 'salon/:slug',
  component: Salon,
  indexRoute: { component: SalonDetails },
  childRoutes: [{
    path: 'customers',
    component: SalonCustomerList,
  },{
    path: 'customer/:customerId',
    component: SalonCustomer,
    indexRoute: { component: SalonCustomerDetails }
    childRoutes: [{
      path: 'booking/:bookingId',
      component: SalonCustomerBooking,
    }]
  }]
}

Which translates into the following URLs

/salon/:slug
/salon/:slug/customers
/salon/:slug/customer/:customerId
/salon/:slug/customer/:customerId/bookings
/salon/:slug/customer/:customerId/booking/:bookingId

I am trying to see the big picture on how to structure the client, but this is a bit beyond me ATM. So I am hoping for some help from the community. But it's really hard to ask these kinds of questions, because of the complexity. Anyway, I will try ...

Let's say user visits the following URL:

/salon/some-slug/customer/123/booking/456

This is my thinking flow ...

First try:

  1. query salon(slug:"some-slug") inside Salon component then render children once I have the data
  2. query salonCustomer(salon:"some-slug", customerId:123) inside SalonCustomer then render children
  3. query salonCustomerBooking(salon:"some-slug", customerId:123, bookingId:456) inside SalonCustomerBooking and render

But that is far from optimal. I am overfetching data and doing multiple roundtrips to the server. Not good.

Second try:

  1. simply query salonCustomerBooking(salon:"some-slug", customerId:123, bookingId:456) and render.

No overfetching, a single roundtrip to the server. Much better. But ... what if some component up the hierarchy wants to render some other data? For example, what if I want to render some basic Customer details in SalonCustomer? Hmm ... not good.

Third try:

  1. batch all queries inside top-level Salon component then ... send all the data down the hierarchy through props?

Basically, this is where it stops for me and I need your help. How, when, where, ... Of course I could go and start experimenting, but it would take me days if not weeks to figure out the best way to do it.

How would you do it?

You can close this topic, because this is NOT an issue with react-apollo. It's just me not seeing the big picture :) And I am sorry for "spamming" the issue tracker with questions.

But I think that it would be good for the community (at least for those of us that don't have much experience with GraphQL clients/servers yet), if we had an example for a more complex app, once the API is finalized. Maybe something like the huge-apps example for react-router. I can do a POC once I understand things.

Oh and BTW: great job on the new docs ;)

@stretchkennedy
Copy link

stretchkennedy commented Sep 6, 2016

We do something along the lines of

query salonCustomerBookingQuery {
  salon(slug: "some-slug") {
    customer(id: 123) {
      booking(id: 456) {
        id
        price
      }
    }
  }
}

for the most important data, pass it down using React.cloneElement(children, { customer: salon.customer }), then run additional queries for less important data like pretty graphs, number-crunching, and things like that. You might find remix-run/react-router#1857 interesting, particularly the bits about cloneElement.

Having said that, I'd be interested to hear from the react-apollo devs, since our architecture was originally based on relay and could probably be improved.

EDIT: https://github.com/reactjs/react-router/blob/master/examples/passing-props-to-children/app.js#L55 might also be useful

@HriBB
Copy link
Author

HriBB commented Sep 6, 2016

@stretchkennedy thanks for the feedback. I know how to pass props to children, but how can I do nested queries with variables? I don't think I've ever seen that in the docs/examples.

I read the docs about batching again and I am wondering if all three queries in first try would be batched together? I don't really mind doing some slight over fetching or some extra queries. But if there's a relatively easy way not to do that, I would really like to know. Where/how to do it with react-apollo in a way so that my head does not explode from all the complexity.

@stretchkennedy
Copy link

With variables, the query would look like

query salonCustomerBookingQuery($salonSlug: String!, $customerId: Int!, $bookingId: Int) {
  salon(slug: $salonSlug) {
    customer(id: $customerId) {
      booking(id: $bookingId) {
        id
        price
      }
    }
  }
}

The GitHunt example has some nested queries in it.

The problem we ran into with batching was dependencies, i.e. component B relied on information from component A's query to render. If you don't have dependencies like that, batching does sound easier.

@jbaxleyiii
Copy link
Contributor

@HriBB this is a great question! I'd love to give a thorough answer. Let me think through a few things and get back to you!

@HriBB
Copy link
Author

HriBB commented Sep 7, 2016

@jbaxleyiii thanks!

I've been thinking about this for some time now, going through all possible scenarios in my head, but there's a lot to take in. It would be good if more people got involved in this discussion, so that we can figure things out together. Maybe we can even create some good recipes for the react-apollo docs.

@HriBB
Copy link
Author

HriBB commented Sep 7, 2016

@stretchkennedy when I write a nested query such as the one above, do I always need to query the entire hierarchy, or can I for example only query salon?

@stretchkennedy
Copy link

@HriBB you can query as much or as little as you like. Check out this example on swapi-graphql.

To give a little more info on what we do, we'd have root fields of salon (takes an id, returns a salon) and salons (takes pagination args and things to filter by, returns an array of salons), customer and customers, booking and bookings, etc. Within each type, we have fields for associated types.

We'd write

query salonCustomerBookingQuery($salonSlug: String!, $customerId: Int!, $bookingId: Int) {
  salon(slug: $salonSlug) {
    id
    name
  }
  customer(id: $customerId) {
    id
    name
  }
  booking(id: $bookingId) {
    id
    price
  }
}

or

query salonCustomerBookingQuery($salonSlug: String!, $customerId: Int!, $bookingId: Int) {
  salon(slug: $salonSlug) {
    id
    name
  }
  customer(id: $customerId) {
    id
    name
    bookings {
      id
      price
    }
  }
}

depending on the UI.

The reason I nested the queries when writing your example is that it looks like you need to know a booking's salon's slug to find the booking. If you can find a booking given only its id, then you can do what I did above.

@deoqc
Copy link

deoqc commented Sep 13, 2016

I won't pretend I'm an expert, but wanted to share my experience so far...

I struggle with this questions every-time and usually can frame it as a trade-off in optimisation (e.g. avoid overfetching, loading from cache) vs decoupling (or modularity).

Bellow I dig deeper in how I usually structure things.


My architecture was inspired/copied from Relay - I even use graphql-relay - but with apollo-client/react-apollo.

Using node interface is much better than creating a graphql end-point to each entity, you actually get this for (almost) free. But I don't see apollo community adopting, or being encouraged by apollo =(.

Every entity I have can be fetched in the node end-point using a (global) id, like so:

const query = gql`
  query ($id: ID!) {
    node(id: $id) {
      id
      ... on MySpecificEntity {
        field
        ...
      }
    }  
  }
`

Now lets exam these 4 cases:

1 -Most decoupling: individual components with individual queries

Now, for example, if I want to fetch a list of stores to display :

list of stores

const query = gql`
  query {
     viewer {
       stores {
         edges {
            node {
              id #always fetch entities id 
              name
              some_other_features
            }
          }
        }
      }
   }  

I just fetch info necessary for the list of stores to display. Now if I click on the store, I have to open it with more info, like its products. So I have:

individual store

const query = gql`
  query ($id: ID!) { # that store id previously fetched
    node(id: $id) {
      id # still need id here to cache properly
      ... on Store {
        name
        field
        products {
          ...
        }
        ...
      }
    }  
  }

Look that the component that render the store only receives one prop: id. It assumes only this, almost nothing, and could virtually be called by any other component in page only passing the store id. This is as much decoupling as it can get.

But w/o query diffing, you may overfetch (like field name). Also, you have to deal w/ delay of loading over network.

2 - Individual components, individual queries connected by fragments

If well, you actually don't want to fetching again every-time you click a store and actually want to fetch all info at once, I would recommend to create a fragment in the store component, and query the list with its fragment but also query the store with fragment.

list of stores

const query = gql`
  query {
     viewer {
       stores {
         edges {
            node {
              id 
              # same as before
              ...StoreFragment # fetch 
            }
          }
        }
      }
   }  

individual store

export const fragment = createFragment(gql`
  fragment StoreFragment on Store {
    name
    field
    products {
      ...
     }
   }
`)

const query = gql`
  query ($id: ID!) { # that store id previously fetched
    node(id: $id) {
      id # still need id here to cache properly
      ... on Store {
        ...StoreFragment
      }
    }  
  }

Apollo will find the data in cache so won't refetch them (actually this is not how it works currently, but will soon be true).

It is not totally decoupled because the list of stores now refers to the fragments in the store. But they are working together w/o much knowledge of each other, you are still only passing id.

Note that will have delay for loading data from cache, but it is much less than over network.

3 and 4 - Passing props, most coupled: one single query, w/ or w/o fragments

Well just fetch everything in parent and the pass props to children. You may have fragments in children to tell which data to fetch, improving modularization (notice that in situation 1 and 2, fragments increased coupling). But this is most coupled situation.

It can introduce bugs: in react-native Navigator, if you pass a props to a scene and then update that props, it won't propagate to the scene. So you may/should do this only if you are working on a page, not to pass props across pages.

Here there is no loading delay.


Bottom line:

  • When a component opens another page, I use 1 or 2 balancing overfetching with network fetch;
  • When inside the same page, I use 3 or 4 balancing my personal taste in the situation =);
  • There are other situations: for an app with tabs, I rely on query batching of each tab for example.

@jbaxleyiii
Copy link
Contributor

This would be a great thing for an article or two! I'm going to close it for now though since a few options have been discussed

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

4 participants