apollo-tote icon indicating copy to clipboard operation
apollo-tote copied to clipboard

Discussion of this API

Open jcheroske opened this issue 6 years ago • 9 comments

Inspired by your work here, I've created two render prop components. One does queries and the other mutations. The API has turned out really nice, and I'd like to talk about it if you're interested.

Jay

jcheroske avatar Nov 29 '17 21:11 jcheroske

Hey, sorry I don't get notifications for some reason 😢

would love to hear more!

peterpme avatar Dec 20 '17 06:12 peterpme

No worries! I'm on the small screen, so I'll keep it brief for now.

The trend seems to be away from dynamic graphql and towards static, and I wanted the components to be aligned with that. The top-level API consists of two component factories: withQuery and withMutation. They are given the AST and return render prop components. Basic usage:

const findUserById = gql`
  query findUserById($userId: ID) {...}
`

const FindUserById = withQuery(findUserById)

const MyComponent = ({userId}) => (
  <FindUserById userId={userId}>
    {({user}) =>
      <h1>{user.name}</h1>
    }
  </FindUserById>
)

Mutations work similarly, except the component just provides a mutate function, similar to the way Apollo works. The main difference is that you can pass props into the mutate component, and pass an object into the mutate function, and all of those values will become variables for the mutation:

const updateUser = gql`...`

const UpdateUser = withMutation(updateUser)

const MyComponent = ({userId}) => (
  <UpdateUser userId={userId}>
    {mutate =>
      <Button onClick={() => mutate({name: 'Foo'}).then(...)}></Button>
    }
  </UpdateUser>
)

jcheroske avatar Dec 20 '17 06:12 jcheroske

@jcheroske I am probably missing a reason why to have those factory functions, what is the benefit instead of passing query as a prop?

danielkcz avatar Jan 02 '18 13:01 danielkcz

I guess there are several benefits. For starters, the Apollo and Facebook teams are encouraging us to use static graphql. People much smarter than I am are saying that's the way to do it, and I'm inclined to follow along. Additionally, the Apollo HOC works this way, and since my components are just thin wrappers around the HOC, it makes sense to follow a similar pattern. I also read about some compile time optimizations that Facebook uses, and those only seem to work with static graphql. Finally, I can't see a compelling reason to make the AST a runtime prop. It's trivial to create insert and update components, for example, and choose between them if you need to select different operations at runtime.

I'll post the code for my components, as well as some examples to illustrate some of the patterns I have found, none of which require a runtime AST prop.

jcheroske avatar Jan 04 '18 00:01 jcheroske

@jcheroske I would totally be open for a PR to make this happen :)

peterpme avatar Jan 04 '18 00:01 peterpme

I'm on the touchscreen again, so I can't post the code right now, but, since you're here, I wanted to tease something. One of the headaches with Apollo is refetching queries after a mutation. You end up with this ugly code in your view layer where the component doing the mutation has to know about the queries that are affected by that mutation and refetch them. What I did was built a trivial dependency graph API that allows you to, when creating a query or mutation, to specify the tables and operations it affects, or is affected by. So, a mutation that adds a user would declare that it affects user inserts. A query that lists users would declare that it depends on user inserts and user deletes. Then, a component can just use the mutation component and refetch queries will be automatically called. It moves ugly refetch queries code out of the view layer and into a reusable database component layer.

What I have found, thinking more broadly, is that being able to componentize queries and mutations enables the creation of a full featured database access layer with minimal effort. Repetitive code, like looking up the currently logged in user and passing it as a query parameter or mutation variable, can be moved into the database components themselves and hidden from clients of the database component. This greatly simplifies the view code.

jcheroske avatar Jan 04 '18 00:01 jcheroske

😍 would LOVE to see what you've got going!!! That is super exciting. I've personally felt that in the past but between features vs. optimization in an early stage scenario, I have to go with the former.

Let me know how I can help make that happen!

peterpme avatar Jan 04 '18 01:01 peterpme

@jcheroske

and since my components are just thin wrappers around the HOC, it makes sense to follow a similar pattern.

Yea, I was thinking about wrapping the HOC too, but it feels kinda dirty. Lately, I am trying to get rid of as many HOC as possible simply because it's a nightmare to navigate through that in React Devtools :)

Considering that Tote would be straightforward implementation without wrapping HOC, I think that passing query directly as a prop works just fine. The query is already AST converted by gql helper, so there is no real optimization coming from having it separately. Or perhaps I am missing out something obvious here...

One of the headaches with Apollo is refetching queries after a mutation. You end up with this ugly code in your view layer where the component doing the mutation has to know about the queries that are affected by that mutation and refetch them.

Yea I remember this headache, but I think it can be approached more reactively instead of some crude dependency tree. I've even gone that far to keep mutation call outside of the component and it's sitting in mobx-state-tree model instead. But it can be in a component too for sure.

import { types } from 'mobx-state-tree'
const UserStore = types
  .model('User', {
    branchId: types.maybe(types.string),
  })
  .actions(self => ({
    selectPickupBranch: flow(function* selectPickupBranch(branchId) {
      const { data } = yield self.apolloClient.mutate(...)
      self.branchId = data.setBranchForOrder.id
    })
  })

Then every query that should be refetched based on changed branchId is simply observing it with MobX and it works like a charm. Nice separation of concern without much of extra hassle. With Tote this would look even better.

const ShopTitle = () => (
  <Observe
    render={store => (
      <ApolloTote
        query={ShopTitleQuery}
        variables={{ branchId: store.user.branchId }}
        render={data => <h4>{getShopTitle(data)}</h4>}
      />
    )}
  />
)

I am well aware this is one of the use cases and it needs more thinking and experimenting, but I think it's an interesting way to go.

danielkcz avatar Jan 04 '18 08:01 danielkcz

I want to share the approach I took regarding render props and graphql. I am not using apollo-tote simply because it's missing many things although it was a great deal of inspiration I gained from here. I basically have a dynamic wrapper around graphql HoC. It works pretty well, especially with TypeScript.

https://gist.github.com/FredyC/655d562e7fd72fd5fa791042fda008c7#gistcomment-2366876

danielkcz avatar Mar 01 '18 19:03 danielkcz