Reindex
TeamBlogDocs

Adding mutations

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 Todo
  • src/mutations/DeleteTodoMutation.js - delete a Todo
  • src/mutations/ChangeTodoCompleteMutation.js - toggle Todo completion
  • src/mutations/ChangeTodoTextMutation.js - change Todo text

We can compose mass operations on Todos using those mutations several times.

ChangeTodoCompleteMutation

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.

ChangeTodoTextMutation

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,
    });
  }
  // ...
}
// ...

AddTodoMutation

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

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')}
      }
    `,
  },
});

Mutations? Mutations!

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.

Topics:

Adding mutations

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 Todo
  • src/mutations/DeleteTodoMutation.js - delete a Todo
  • src/mutations/ChangeTodoCompleteMutation.js - toggle Todo completion
  • src/mutations/ChangeTodoTextMutation.js - change Todo text

We can compose mass operations on Todos using those mutations several times.

ChangeTodoCompleteMutation

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.

ChangeTodoTextMutation

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,
    });
  }
  // ...
}
// ...

AddTodoMutation

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

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')}
      }
    `,
  },
});

Mutations? Mutations!

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.