Wrangling the client store with the Relay Modern updater function.

Cameron Moss
Entria
Published in
7 min readJan 26, 2018
How ya gonna do that Relay?

In this post I’m going to provide some examples and patterns around RelayModern’s documentation on the store; specifically, how to update the store using the updater function inside a mutation. Mastering the updater function in Relay is the equivalent of mastering the reducer in Redux.

Acknowledgements: If it weren’t for this post on updating the store and other posts like those by Sibelius Seraphini — and his ReactNavigationRelayModern repo — I would no doubt have given up on Relay by now.

Required:

Before we get into the updater, do yourself a favor and be sure to install the relay-devtools. Without this tool, you will get nowhere fast!

TL;DR

Why updater?

Relay controls a lot of what the store is doing behind the scenes. When you query data you don’t have to think about it much. When you start writing mutations however, you’ll start to notice that your store, and thus your views, aren’t always updating to reflect the mutated data.

The linked data of a graphQL client store is not trivial to update when the server returns just a small sliver of what comprises the entire store. The Relay global ID helps, but is not a silver bullet. One way to leverage it is to design the mutation to return the parent.

ThinkPost and Comment. If you have an addPostComment mutation you could design the mutation to return all the comments on the post:post{comments{text}, or just the one added: comment{text}.

If it returns the entire post with the full array of comments, Relay will automatically find and replace the entry in the store along with the new comments field. But if it returns comment you’ll need to provide more information to get it placed on the correct Post in the store.

So why not just return the entire post?

First, it might be that the comment also needs to be linked to the user who wrote it. Next, you’re going to end up writing a lot of extra server logic to fetch the parent while at the same time increasing the data out.

I believe you are better off always returning the mutated record. You’ll keep things simple and lean — and the majority of your mutations, especially updates, still probably won’t need the updater.

Scalars and Interfaces — not plain data objects!

The key to knowing when you need to use the updater is if you are adding or removing new objects to a field, array, or connection of another node. But in the updater function, objects are managed by interfaces.

So in the updater you mainly have scalars and three types of interfaces

Scalars are treated like regular js primitives in the updater but objects are not. If we want to get the the rate from the above we do

const rate = opening.getValue('rate') // "US$9.80"

But if we want to access the site, Relay expects you to access it as a linkedRecord which returns RecordProxy interface.

const site = opening.getLinkedRecord('site') // RecordProxy{...}

Actually, the opening above itself is a RecordProxy. You never deal with plain js object representations of data in the updater, only RecordProxy interfaces. We’ll deal with connections later.

The important thing to see here is that if you are accessing anything other than scalars or connections, you need to use getLinkedRecord() , or for an array field getLinkedRecords():

const applicants = opening.getLinkedRecords('applicants')// [RecordProxy, RecordProxy, ...] a plain array of RecordProxies

Updating fields

Let’s start with a somewhat easy example to get the hang of things. Let’s add a simple anonymous Comment to a Post.

mutation {
addPostComment(input: {postID: 3, text: 'great write!'}) {
comment{
text
}
}
}

Here we’ve designed the mutation to return the Comment type without any other information. It’s an orphaned record in that the return payload gives no context for its placement in the store. Again, we aren’t updating existing data, we are linking new data.

Thinking of mutation return value as an orphan is a good way to conceptualize where Relay decides to place the value in the store. Because there is nowhere to put it, it leaves the record at root, keyed by the mutation name itself, and that is where you have to find it.

How to get the mutation payload

When you use the updater function you are overriding Relay so its up to you to grab the payload at root and move it into the store somewhere.

updater: store => {
const payload = store.getRootField('addPostComment')

Again, like the return value of getLinkedRecord() thepayload is not just a plain js object, but a Relay configured proxy of the object. If you console.log it you’ll see what I mean.

If your payload is nested in a field such as result

result: {
id: "VXNlcjo2Yzg0ZmI5MC0xMmM0LTExZTEtODQwZC03YjI1YzVlZTc3NWE="
text: "great write!"
}

you need to do an extra step of actually getting the comment proxyRecord.

updater: store => {
const payload = store.getRootField('addPostComment')
const comment = payload.getLinkedRecord('result')

Now we have a Relay store representation of a Comment that can be linked to a particular Relay store representation of a Post.

So where do we find that? There is no reference to the user whatsoever in the mutation return.

You should always be able find aNode in the store easily, no matter how deeply nested it might be. That’s why the Relay global ID exists. There’s a very simple method for that store.get().

post = store.get(id)

Simple enough? But where do we find the id for the post in question?

Passing context to the mutation

The fact is, the Relay global ID of the parent you’re linking the mutation result to is nearly always available where you are calling the mutation. So I use a simple pattern of passing any needed global IDs to the mutation commit method like so:

AddPostComment.commit(
{
text: 'great write!',
postId: 3,
},
{
idPost: post.id,
},
)

Notice how idPost is the Relay global ID(to distinguish from the postId primary key for the Post table). You could call it parentGlobalID if you want to, that’s up to you. Now the global ID is in the scope of the update function:

function commit({ text, postId }, { idPost }) {
return commitMutation(getEnvironment(), {
mutation,
variables: { input: {text, postId}},
updater: store => {
const payload = store.getRootField('addPostComment')
const comment = payload.getLinkedRecord('result')
const post = store.get(idPost) // <--------

Now to add the comment to the comments field on the Post. Lets first get the existing comments array.

const postComments = post.getLinkedRecords('comments') || []

Be sure to provide the default empty array in case the comments field doesn’t exist in your store. It’s fairly common case that the view with array hasn’t rendered and thus doesn’t exist in the store yet.

Now we finish it with some ES6 immutability helpers( merely pushing to the array of postComments will not work).

const newPostComments = [...postComments, comment]
post.setLinkedRecords(newPostComments, 'comments')

Remember, these are all RecordProxies, so don’t treat them like js objects. If it helps you to say commentProxy that’s how they do it in the docs.

Finally lets do a scalar update by incrementing the commentCount field on post

post.setValue(post.getValue('commentCount') + 1), 'commentCount')

All together now:

function commit({ text, postId }, { idPost }) {
return commitMutation(getEnvironment(), {
mutation,
variables: { input: {text, postId}},
updater: store => {
const payload = store.getRootField('addPostComment')
const comment = payload.getLinkedRecord('result')
const post = store.get(idPost)
const postComments = post.getLinkedRecords('comments') || []
const newPostComments = [...postComments, comment]
post.setLinkedRecords(newPostComments, 'comments')
post.setValue(
post.getValue('commentCount') + 1),
'commentCount'
)
}
}
}

Connections

Now we are the New York Times and want to use a connection instead of a normal array for comments so we can paginate them. When you want to find a connection in the store you use the connection key defined in your query:

comments(first: $count) @connection(
key: “PostDetails_comments”
filters: []
)

from then on it’s all ConnectionHandler methods:

const comments = ConnectionHandler.getConnection(
post,
'PostDetails_comments'
)

then:

const edge = ConnectionHandler.createEdge(
store,
comments,
comment,
'CommentsEdge'
)
ConnectionHandler.insertEdgeAfter(comments, edge)

The API speaks for itself at this point. There are only a few catches.

Fields with arguments

If you need to update a field that takes an argument, you’ll notice in the relay-devtools that the key of the field contains the argument.

comments{isMember: false} : [...]

If you toggle the boolean isMember you see separate connection field on post altogether so you have to distinguish, when adding a comment, which connection that its going to. When referencing a field like this, most updater functions take a last argument as the argument.

const comments = ConnectionHandler.getConnection(
post,
'PostDetails_comments',
{isMember: false},
)

Removing Things

If you have the global ID of anything and just want to wipe it from the store you can do so

store.delete(id)

If you want to remove something from a connection

ConnectionHandler.deleteNode(friends, id);

Bonus Relay tips

If you are tired of seeing Relay’s runtime generated files, most editors can be configured to hide them, vs code config is:

"files.exclude": {"**/__generated__": true}

If you are changing your server schema often, guess what? You better have some way of updating it locally. Try this. -> $ node get_schema.js

Thanks for reading! Please comment with suggestions, comments.

--

--

Cameron Moss
Entria
Writer for

all things js, react-native + redux, foster parenting, social justice