Relay: [meta] Simpler Mutations API

Created on 2 Nov 2015  路  16Comments  路  Source: facebook/relay

This is a meta-task to track progress toward making Relay mutations simpler to both understand and to define in common cases.

In a client/server system there is an unavoidable complexity in handling writes:

  • Making network requests
  • Handling error (and possibly retries)
  • Sequencing possibly overlapping writes
  • Handling optimistic changes and reverting them on error, or clearing them on success
  • Handling server response payloads
  • (server) Implementing business logic to process incoming writes

Relay handles the vast majority of this complexity on behalf of developers. The tradeoff is that GraphQL mutations are abstracted from the underlying data store, and therefore the system _cannot automatically know what changed_. This requires the developer to tell the system what changed in the form of mutation configs (getConfigs).

There are several avenues for exploration:

  1. Simplify the process of defining mutation configs. @steveluscher proposed creating helper functions that would replace the need for the mutation config objects.
  2. Require product developers to manually convert the mutation payload into a set of change operations (e.g. set('record', 'field', 'value') or append('connection', 'edgeId')). If Relay could understand a small set of these change operations, the community could work to define helpers for converting payloads into this form.
  3. Create a standard response format for GraphQL mutations to match the change operations from 2, such that mutations would return a description of what changed. This would not reduce the work of defining mutations, but would allow defining the changes once instead of once per client.

It's important to note that the current mutations API is heavily skewed toward practicality: it has allowed us to iterate quickly and produce resilient applications. We're interested in making this API better and welcome contributions from the community. In particular, the best form of contribution is either links to prototypes or pull requests, which will help us and the community understand the practical tradeoffs of any alternative APIs.

enhancement mutations

Most helpful comment

In terms of getConfigs, the biggest issue for me is that rangeBehaviors don't really work that well. Essentially a connection can be augmented such that it's filterable and sortable, by an arbitrary number of arguments. It's not enough to be able to take a single argument and decide whether to append/prepend/ignore. We need to be able to look at all the arguments (at the same time) and return a sort key that tells Relay where to add it to the list (or whether to remove it).

rangeBehaviours: (edge, connectionArgs) => {
  // perform some calculations and return a sort key, or null to remove it from the list
}

Of course, it wouldn't be enough to just run this against the new edges, it'd need to be done for the existing edges in the connection too.

At this point, there's no real reason that these behaviours need to be defined in the mutation at all, since it's the component querying on that connection that ultimately cares whether an edge should be in it or not. So it might be preferable to have a way of annotating the actual GraphQL query with a sortKey function, maybe like this:

fragment on Foo {
  bars(
    first: $count
    filter1: $filter1
    filter2: $filter2
    sortBy: $sortBy
  ) @relay(getSortKey: mySortFunc) {
     // Normal edge/node query here
  }
}

All 16 comments

In terms of getConfigs, the biggest issue for me is that rangeBehaviors don't really work that well. Essentially a connection can be augmented such that it's filterable and sortable, by an arbitrary number of arguments. It's not enough to be able to take a single argument and decide whether to append/prepend/ignore. We need to be able to look at all the arguments (at the same time) and return a sort key that tells Relay where to add it to the list (or whether to remove it).

rangeBehaviours: (edge, connectionArgs) => {
  // perform some calculations and return a sort key, or null to remove it from the list
}

Of course, it wouldn't be enough to just run this against the new edges, it'd need to be done for the existing edges in the connection too.

At this point, there's no real reason that these behaviours need to be defined in the mutation at all, since it's the component querying on that connection that ultimately cares whether an edge should be in it or not. So it might be preferable to have a way of annotating the actual GraphQL query with a sortKey function, maybe like this:

fragment on Foo {
  bars(
    first: $count
    filter1: $filter1
    filter2: $filter2
    sortBy: $sortBy
  ) @relay(getSortKey: mySortFunc) {
     // Normal edge/node query here
  }
}

I feel like given a choice, though, that sort key behavior is more a property of the connection field than of the query.

You wouldn't really want to define it differently if you were using the same connection in more than one place, and ideally you'd define it on the server anyway.

That's true, I didn't think about pushing it further down the stack. It might make optimistic responses trickier though - If the client knows about the sort function, they could be handled more cleanly too.

Since you're taking a look at this right now, I'd say one of the things I dislike the most about the current API is that the fat-query is a client-side concern. If you're implementing an AddCommentMutation on the client, it's likely that you'll specify a fat query like: post { comments }, since that's what the client is up to. However, maybe there's another part of the server graph that is affected, like analytics { totalCommentCount }. The server-side developer _knows_ that she has affected the analytics node, and can even add the analytics node to the mutation payload, but the client will continue neglecting to update that node until the client fat query is fixed.

Conceivably, the fat query could simply be the mutation payload definition, but that precludes the ability to narrow the affected fields via selections (e.g. analytics vs. analytics { totalCommentCount }). So maybe the fat query is a string attached to the payload type as metadata...?

I'm going to fold some other issues into this one as a checklist. This will be make things more manageable, seeing as these all end up being interrelated:

  • [ ] Add an onProgress-type callback for mutations with files (was #789)
  • [ ] Support adding multiple edges to a range in one operation (was #783)
  • [ ] Handle plural file uploads (was #586)
  • [ ] Make RANGE_ADD easier to use by solving fragile dependency on having also queried for the connection (was #542)
  • [ ] Fill in gaps in Relay{Mutation,Query}Request documentation (was #581)
  • [ ] Make mutations and returned cursors work with sorted/filtered connections (was #766)
  • [ ] Consider renaming RelayMutation's getVariables method (and maybe getFatQuery) to reduce confusion (was @#711)
  • [ ] Add @relay(pattern: true) to docs (was #647)
  • [ ] Rename REQUIRED_CHILDREN to EXTRA_FRAGMENT and document its use (was #237)
  • [ ] Address server-side configs (was #826, see also #489, #293, #125)

(More to come, but I'm still curating...)

Hey @wincent, if I wanted to run some more mutation-related ideas/questions past you, how would you prefer to see it? Just comment here? For example, I'm curious to hear what the Relay team's thoughts are about scenarios where the client _can't_ know in advance which nodes might be affected by a mutation...

@NevilleS: I'm fine with you creating separate issues, as they provide us with a space to explore each topic. But just bear in mind that they might ultimately get closed and folded into the checklist above if it they end up being something that we can tackle as part of the mutations overhaul.

Hi @wincent
Do you have any ETA about multiple file uploads?

@eugenehp: I don't have any work in progress on that specific front, but you should be able to do this today using the workaround noted in #586 (keying the files in the FileMap, as opposed to an array). If somebody feels urgency to get array support baked in sooner, we'd love to take a look at a PR.

Regarding points 2 and 3 above in the OP, I feel like a nice optimum might be to define those change operations in userspace, as e.g. reducers that operate on the store, if this is applicable.

This would then allow both a high level API for defining those operations from the server, but also offer the option of dropping to a lower level API in case the existing set of verbs is insufficient, but handling the latter entirely in user space.

Would it be possible for RANGE_ADD to have behaviors like after or before that specify cursors? I am, for example, inserting an edge into an alphabetically sorted list.

(edit: looks like this was previously discussed at #293)

I wouldn't hold my breath on that - trying to design an API for RANGE_ADD to allow for (basically) arbitrary collection mutations via some combination of JSON config params seems like a losing battle... the real solution here is to allow for arbitrary mutations of the store via a controlled API, which is what you see in the Relay 2 presentations 馃憤

Hi @NevilleS. Long time! I've been out of the Relay loop a bit lately: Relay 2 presentations? Are you talking about talks (like https://speakerdeck.com/wincent/relay-2-simpler-faster-more-predictable) or a feature called "presentations"?

I'm seeing something about a new "imperative mutations API" in this talk (will watch the YouTube later鈥攅xciting!)

Yeah, I was talking about the presentations Greg and Joe gave recently about some Relay 2 goals, one of which is a different approach to mutations, hopefully you found some of that? Joe's talk at React Rally had an example.

Done thanks to @wincent - check out Relay.GraphQLMutation.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mike-marcacci picture mike-marcacci  路  3Comments

jstejada picture jstejada  路  3Comments

scotmatson picture scotmatson  路  3Comments

rayronvictor picture rayronvictor  路  3Comments

leebyron picture leebyron  路  3Comments