A brief overview of the relay store and its updater functions
Timon
Posted on January 16, 2024
This Article will give you a brief introduction on how we can update the relay store in real time without waiting for any server response. Before reading it, you should have a brief understanding on how relay works and some javascript knowledge won’t hurt as well.
When working on a drag and drop implementation for the Carla Backoffice, we quickly realized, that waiting for a server response is not always good enough when you want to build good and user-friendly software. What if we don’t want to wait for the network request to be finished? We already know what will change so we could update it ourself. This would provide a smoother user experience, by making it appear like the mutation has already taken place, even though the server-side data may not have changed yet. To understand how we can do that with relay, we first need to understand what the relay store is.
As by definition it stores the payload of our GraphQl queries to help reduce the number of network requests. It is automatically updated when a query is made.
To do that relay provides us so called updater functions we can pass to the UseMutationConfig. We will look at optimisticResponse and optimisticUpdater. With the help of those two we can let the store know what the actual result will look like before it’s there or manipulate the store data directly. No matter what we do in those functions, it will be rolled back or overwritten if the server responds with an error or different data.
In the following we will look at some code examples. I will link the whole project soon but for now you should be good with the snippets I put here. Starting with the parts of the graphQl schema which are interesting for us.
type Query {
_: Int
todos(options: PageQueryOptions): TodosPage
todo(id: ID!): Todo
}
type Mutation {
_: Int
updateTodo(id: ID!, input: UpdateTodoInput!): Todo
}
type TodosPage {
data: [Todo]
links: PaginationLinks
meta: PageMetadata
}
type Todo {
id: ID
title: String
completed: Boolean
user: User
}
input UpdateTodoInput {
title: String
completed: Boolean
}
type User {
id: ID
name: String
todos(options: PageQueryOptions): TodosPage
}
Yes! It’s going to be another todo app. Nothing fancy we just need some use cases we can look at. The only thing we will do is updating the complete status of one or more todo items.
Optimistic Response
As mentioned before, in the optimisticResponse function we can let the store know what the result will most likely look like.
The following code snippet shows the query we use to fetch all todos.
const TodoQuery = graphql`
query AppTodoQuery {
todos {
data {
id
title
completed
}
meta {
totalCount
}
}
}
`;
const preloadedQuery = loadQuery(RelayEnvironment, TodoQuery, {
options: {
paginate: {
page: 1,
limit: 5,
},
},
});
There is nothing out of the ordinary here. With that said let’s get to the more interesting part. The actual mutation we use to update the todo.
const updateTodoMutation = graphql`
mutation AppUpdateTodoMutation($id: ID!, $input: UpdateTodoInput!)
@raw_response_type {
updateTodo(id: $id, input: $input) {
id
title
completed
}
}
`;
The first difference to any mutation you might have used before without using optimistic response is the @raw_response_type directive. We need that to make it work.
const [commitUpdateTodo, isMutating] = useMutation(updateTodoMutation)
<TodoListItem
key={id}
completed={completed}
onChangeCheckbox={() =>
commitUpdateTodo({
variables: {
id: id,
input: { completed: !completed },
},
optimisticResponse: {
updateTodo: {
id: id,
completed: !completed,
title: title,
},
},
})
}>{title}</TodoListItem>
To mimic the response we keep everything the same but negate the value for the completed entry.
And thats as easy as it gets. But there is more!
For some cases it’s not enough to only touch the data returned by the mutation and we want to do more complicated stuff with, for example, the other todo items. For that we can use the optimistic updater.
Optimistic Updater
Let us first look at how we can achieve the same we did with the optimisticResponse above. The optimisticUpdater is giving us a RecordSourceSelectorProxy we call selector here. With that interface we can do changes and get data from the relay store. As you can see, we use this to get the todo record we try to change. And again, we don’t actually get the the record itself but rather an interface we can use to manipulate the data. In that case it’s a RecordProxy. Thats why we can use the setValue function to negate the completed value. Those interface make sure our data is immutable.
<TodoListItem
key={id}
user={user}
completed={completed}
onChangeCheckbox={() =>
commitUpdateTodo({
variables: {
id: id,
input: { completed: !completed },
},
optimisticUpdater: (selector) => {
const todo = selector.get(id);
todo.setValue(!completed, "completed");
},
})
}>{title}</TodoListItem>
Now Imagine for our todo list, we update all the todos created by a user as soon as one of them is completed. Sounds like a weird example but you never know what the stakeholder comes up with. (And there is no other use case I can think of with using the free graphQl api I found). We still only get the record returned we mutated so we cant use optimisticResponse. In that case we need to use the optimisticUpdater.
<TodoListItem
key={id}
user={user}
completed={completed}
onChangeCheckbox={() =>
commitUpdateTodo({
variables: {
id: id,
input: { completed: !completed },
},
optimisticUpdater: (selector) => {
const root = selector.getRoot();
const todosPage = root.getLinkedRecord("todos");
const todos = todosPage.getLinkedRecords("data");
todos.filter((todo) =>
todo.getLinkedRecord("user").getValue("id") === user.id)
.map((filteredTodo) =>
filteredTodo.setValue(!completed, "completed")
);
},
})
}>{title}</TodoListItem>
We now need to get the store root so we can get all todos. One thing which helped me knowing what I have and what I need to do is logging the getType function on my RecordProxies. With that I can double check with the Schema and be sure I am on the right track of doing what I want to do. Let’s look at the example above. After calling getRoot I get the RecordProxy representing the root of our store. With that I can get the todos interface.
Looking at the schema I can see that they are of the TodosPage type which looks like this:
type TodosPage {
data: [Todo]
links: PaginationLinks
meta: PageMetadata
}
From there we can use getLinkedRecords to get the data and use simple array functions to filter for the same user Id and map to change the value.
Conclusion
I hope this helped you to get some understanding on how and when to use relays updater functions. There are some downsides you should be aware of. For the optimisticResponse you need to be careful when having big queries. The @raw_response_type directive can cause performance issues.
For the optimisticUpdater my biggest concern is the complexity which will be added to the codebase. You would need to learn about the interfaces used to manipulate the store.
Posted on January 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.