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:
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:
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.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.
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:
onProgress-type callback for mutations with files (was #789)RANGE_ADD easier to use by solving fragile dependency on having also queried for the connection (was #542)Relay{Mutation,Query}Request documentation (was #581)RelayMutation's getVariables method (and maybe getFatQuery) to reduce confusion (was @#711)@relay(pattern: true) to docs (was #647)REQUIRED_CHILDREN to EXTRA_FRAGMENT and document its use (was #237)(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.
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).
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: