We are displaying our Todos, now we can enable the mutations that we need. The mutations that we need are:
src/mutations/AddTodoMutation.js
- adding a new Todosrc/mutations/DeleteTodoMutation.js
- delete a Todosrc/mutations/ChangeTodoCompleteMutation.js
- toggle Todo completionsrc/mutations/ChangeTodoTextMutation.js
- change Todo textWe can compose mass operations on Todos using those mutations several times.
Let’s start by looking at the source code of src/mutations/ChangeTodoCompleteMutation.js
import Relay from 'react-relay';
export default class ChangeTodoStatusMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation{ updateTodo }`;
}
getVariables() {
return {
id: this.props.id,
complete: this.props.complete,
};
}
getFatQuery() {
return Relay.QL`
fragment on _TodoPayload {
changedTodo {
complete,
},
}
`;
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
changedTodo: this.props.id,
},
}];
}
getOptimisticResponse() {
return {
changedTodo: {
id: this.props.id,
complete: this.props.complete,
},
};
}
}
Let’s look at this method one-by-one.
getMutation()
must return a root mutation field, similar to Route.
Reindex provides basic CRUD methods for all user defined types, for updating
Todo updateTodo
is created.getVariables()
returns an object that is passed as input
to the mutation.
In this mutation we need to pass a todo id and its new complete
status.getFatQuery()
must return a query fragment on the result of the mutation.
Like with connection, type of result of mutation is generated by Reindex and
is called _TodoPayload
in our case. It has updated object in changedTodo
field, we know that only complete
can change in this mutation, so we only
include it.getConfigs()
returns a list of Relay Mutation Configs, which indicate how
Relay should propagate that mutation into the store. FIELDS_CHANGE
is the
simplest kind of mutation, it just updates data in client store by id. As we
have mentioned, updated todo will be in changedTodo
and we indicate that in
fieldIDs
.getOptimisticResponse()
returns a fake payload so that Relay can apply the
update optimistically.We can now update our src/components/Todo.js
to use that mutation.
// ...
import ChangeTodoStatusMutation from '../mutations/ChangeTodoCompleteMutation';
// ...
class Todo extends Component {
// ...
handleCompleteChange = () => {
Relay.Store.commitUpdate(
new ChangeTodoStatusMutation({
id: this.props.todo.id,
complete: !this.props.todo.complete,
}),
);
}
// ...
}
// ...
We can also make src/components/TodoList.js
use that mutation for “toggle all”.
// ...
import ChangeTodoStatusMutation from '../mutations/ChangeTodoCompleteMutation';
// ...
class TodoList extends Component {
// ...
handleToggleAllChange = () => {
const todoCount = this.props.todos.count;
const done = this.props.todos.edges.reduce((next, edge) => (
next + (edge.node.complete ? 1 : 0)
), 0);
let setTo = true;
if (todoCount === done) {
setTo = false;
}
this.props.todos.edges
.filter((edge) => edge.node.complete !== setTo)
.forEach((edge) => Relay.Store.commitUpdate(
new ChangeTodoStatusMutation({
id: edge.node.id,
complete: setTo,
})
));
}
// ...
}
export default Relay.createContainer(TodoList, {
fragments: {
todos: () => Relay.QL`
fragment on _TodoConnection {
count,
edges {
node {
id,
complete,
${Todo.getFragment('todo')}
}
}
}
`
},
});
Note how we dispatch the mutation multiple times to toggle all todos. We had to
also add id
to our fragment, so we can pass it to the mutation.
The mutation for changing todo text (src/mutations/ChangeTodoTextMutation.js
)
is very similar:
import Relay from 'react-relay';
export default class ChangeTodoTextMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation{ updateTodo }`;
}
getVariables() {
return {
id: this.props.id,
text: this.props.text,
};
}
getFatQuery() {
return Relay.QL`
fragment on _TodoPayload {
changedTodo {
text,
},
}
`;
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
changedTodo: this.props.id,
},
}];
}
getOptimisticResponse() {
return {
changedTodo: {
id: this.props.id,
text: this.props.text,
},
};
}
}
We can update src/components/Todo.js
with it too.
// ...
import ChangeTodoTextMutation from '../mutations/ChangeTodoTextMutation';
// ...
class Todo extends Component {
// ...
handleInputSave = (text) => {
Relay.Store.commitUpdate(
new ChangeTodoTextMutation({
id: this.props.todo.id,
text: text,
}),
);
this.setState({
isEditing: false,
});
}
// ...
}
// ...
Adding a Todo is more complex. The reason for this is that we need to update
not only the state of a Todo object that we will create, but also a connection
where it is stored - the count of Todos will change, as well as the listing of
Todo nodes in edges
.
import Relay from 'react-relay';
export default class AddTodoMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`fragment on ReindexViewer {
id
allTodos {
count,
}
}`
};
getMutation() {
return Relay.QL`mutation{ createTodo }`;
}
getVariables() {
return {
text: this.props.text,
complete: false,
};
}
getFatQuery() {
return Relay.QL`
fragment on _TodoPayload {
changedTodoEdge,
viewer {
id,
allTodos {
count
}
}
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentID: this.props.viewer.id,
connectionName: 'allTodos',
edgeName: 'changedTodoEdge',
rangeBehaviors: {
'': 'prepend',
},
}, {
type: 'FIELDS_CHANGE',
fieldIDs: {
viewer: this.props.viewer.id,
},
}];
}
getOptimisticResponse() {
return {
changedTodoEdge: {
node: {
text: this.props.text,
complete: false,
},
},
viewer: {
id: this.props.viewer.id,
allTodos: {
count: this.props.viewer.allTodos.count + 1,
},
},
};
}
}
In order to perform this mutation, we need some data that might not be available
to the component - the id of viewer
object and count of allTodos
connection.
Therefore we need to specify fragments for the mutation same way as we specify
them for containers.
Our configs are more complex this time too - we need to add our new Todo to
a connection, so we use RANGE_ADD
mutation config. Relay expects an edge
to
be passed in payload, not just a Todo, Reindex provides changedTodoEdge
for
this. Lastly we need to fetch updated connection count from the server and for
this viewer
field is available for every payload.
In our optimistic update we increment the count of allTodos, so that we change our “total” display without any delay.
Let’s use our mutation in TodoApp
.
// ...
import AddTodoMutation from '../mutations/AddTodoMutation';
// ...
class TodoApp extends Component {
// ...
handleInputSave = (text) => {
Relay.Store.commitUpdate(
new AddTodoMutation({
text,
viewer: this.props.viewer,
}),
);
}
// ...
}
export default Relay.createContainer(TodoApp, {
fragments: {
viewer: () => Relay.QL`
fragment on ReindexViewer {
allTodos(first: 1000000) {
count,
edges {
node {
id,
complete
}
}
${TodoList.getFragment('todos')}
},
${AddTodoMutation.getFragment('viewer')}
}
`,
},
});
DeleteTodoMutation
is similar, but we use NODE_DELETE
instead of
RANDE_ADD
.
import Relay from 'react-relay';
export default class DeleteTodoMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`fragment on ReindexViewer {
id
allTodos(first: 1000000) {
count,
}
}`
};
getMutation() {
return Relay.QL`mutation{ deleteTodo }`;
}
getVariables() {
return {
id: this.props.id,
};
}
getFatQuery() {
return Relay.QL`
fragment on _TodoPayload {
id,
viewer {
id,
allTodos {
count,
}
}
}
`;
}
getConfigs() {
return [{
type: 'NODE_DELETE',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'allTodos',
deletedIDFieldName: 'id',
}, {
type: 'FIELDS_CHANGE',
fieldIDs: {
viewer: this.props.viewer.id,
},
}];
}
getOptimisticResponse() {
return {
id: this.props.id,
viewer: {
id: this.props.viewer.id,
allTodos: {
count: this.props.viewer.allTodos.count - 1,
},
},
};
}
}
DeleteTodoMutation
can be used in Todo.js
, but it requires some data from
viewer. Therefore we’d need to create a new fragment in the component, that will
request that required data.
// ...
import DeleteTodoMutation from '../mutations/DeleteTodoMutation';
// ...
class Todo extends Component {
// ...
handleDestroyClick = () => {
Relay.Store.commitUpdate(
new DeleteTodoMutation({
id: this.props.todo.id,
viewer: this.props.viewer,
}),
);
}
// ...
}
export default Relay.createContainer(Todo, {
fragments: {
todo: () => Relay.QL`
fragment on Todo {
id,
text,
complete
}
`,
viewer: () => Relay.QL`
fragment on ReindexViewer {
${DeleteTodoMutation.getFragment('viewer')}
}
`
}
});
Similarly, we need to include this viewer fragment in TodoList
.
export default Relay.createContainer(TodoList, {
fragments: {
todos: () => Relay.QL`
fragment on _TodoConnection {
count,
edges {
node {
id,
complete,
${Todo.getFragment('todo')}
}
}
}
`,
viewer: () => Relay.QL`
fragment on ReindexViewer {
${Todo.getFragment('viewer')}
}
`,
},
});
And finally in the TodoApp
. We will also include “clear completed”
functionality.
// ...
import DeleteTodoMutation from '../mutations/DeleteTodoMutation';
// ...
class TodoApp extends Component {
// ...
handleClearCompleted = () => {
this.props.viewer.allTodos.edges
.filter((edge) => edge.node.complete)
.forEach((edge) => Relay.Store.commitUpdate(
new DeleteTodoMutation({
id: edge.node.id,
viewer: this.props.viewer,
})
));
};
// ...
}
export default Relay.createContainer(TodoApp, {
fragments: {
viewer: () => Relay.QL`
fragment on ReindexViewer {
allTodos(first: 1000000) {
count,
edges {
node {
id,
complete
}
}
${TodoList.getFragment('todos')}
},
${TodoList.getFragment('viewer')}
${AddTodoMutation.getFragment('viewer')}
${DeleteTodoMutation.getFragment('viewer')}
}
`,
},
});
We have enabled mutations in our Todo application and we can now create and update todos. You can also find the final code on GitHub.
In the next section we’ll deploy our application.