# Relay Tutorial: Creating a basic UI

Step 1: Create relay-todolist/src/common/UserQuery.js

Let us create our first component: UserQuery which is a query Relay fragment shared by all our routes. Replace relay-todolist/src/common/UserQuery.js with the following code:


import Relay from 'react-relay';

const UserQuery = {
  user: (Component, variables) => Relay.QL`
    query {
      user {
        ${Component.getFragment('user', {...variables})}
      }
    }
  `,
};

export default UserQuery;

Step 2: Create Mutation: relay-todolist/src/mutations/AddTodoMutation.js

AddTodoMutation defines a mutation for adding new todos by calling the addTodo mutation in the GraphQL query defined in getMutation() method. Most importantly we define the connectionName, edgeName, type and rangeBehaviors in this mutation and pass in the new todo item as a variable in getVariables().


import Relay from 'react-relay';

export default class AddTodoMutation extends Relay.Mutation {
  static fragments = {
    user: () => Relay.QL`
      fragment on User {
        id,
      }
    `
  }

  getMutation() {
    return Relay.QL`mutation { addTodo }`;
  }

  getFatQuery() {
    return Relay.QL`
      fragment on AddTodoPayload {
        todoEdge,
        user {
          id
        },
        error
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'RANGE_ADD',
      parentName: 'user',
      parentID: this.props.user.id,
      connectionName: 'todos',
      edgeName: 'todoEdge',
      rangeBehaviors: {
        '': 'append',
        'status(any)': 'append',
        'status(completed)': null
      }
    }];
  }

  getVariables() {
    return {
      todo: this.props.todo,
    };
  }
}

Step 3: Create Mutation: relay-todolist/src/mutations/RemoveTodoMutation.js

RemoveTodoMutation defines a mutation for removing existing todos by calling the removeTodo mutation in the GraphQL query defined in getMutation() method. Most importantly we define the type, parentName, parentID, connectionName and deletedIDFieldName in this mutation and pass in the id of the todo item in getVariables().


import Relay from 'react-relay';

export default class RemoveTodoMutation extends Relay.Mutation {
  static fragments = {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
      }
    `,
    user: () => Relay.QL`
      fragment on User {
        id,
      }
    `
  }

  getMutation() {
    return Relay.QL`mutation { removeTodo }`;
  }

  getFatQuery() {
    return Relay.QL`
      fragment on RemoveTodoPayload {
        todoIdToBeDeleted,
        user {
          id
        }
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'NODE_DELETE',
      parentName: 'user',
      parentID: this.props.user.id,
      connectionName: 'todos',
      deletedIDFieldName: 'todoIdToBeDeleted',
    }];
  }

  getVariables() {
    return {
      id: this.props.todo.id,
    };
  }
}

Step 4: Create Mutation: relay-todolist/src/mutations/UpdateTodoMutation.js

UpdateTodoMutation defines a mutation for updating existing todos by calling the updateTodo mutation in the GraphQL query defined in getMutation() method. Most importantly we define the type and fieldIDs that change in the getConfigs() method. We also pass in 3 variables id, todo and completed via the getVariables() method. We also construct an optimistic response in the getOptimisticResponse() method.


import Relay from 'react-relay';

export default class UpdateTodoMutation extends Relay.Mutation {
  static fragments = {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
        todo,
        completed
      }
    `
  };

  getMutation() {
    return Relay.QL`mutation { updateTodo }`;
  }

  getFatQuery() {
    return Relay.QL`
      fragment on UpdateTodoPayload {
        todo {
          id,
          todo,
          completed
        }
      }
    `;
  }

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        todo: this.props.todo.id
      },
    }];
  }

  getVariables() {
    return {
      id: this.props.todo.id,
      todo: this.props.todoText || this.props.todo.todo,
      completed: this.props.completed,
    };
  }

  getOptimisticResponse() {
    return {
      todo: {
        id: this.props.todo.id,
        todo: this.props.todoText || this.props.todo.todo,
        completed: this.props.completed,
      }
    };
  }
}

Step 5: Create relay-todolist/src/components/Todo.js

Let us create our first component: Todo. Paste the following code into the newly created Todo.js file:


import React from 'react';
import Relay from 'react-relay';
import { withRouter } from 'react-router';

import {
  Col,
  Row,
  Grid,
  Icon,
  Button,
  Checkbox,
  ButtonGroup,
} from '@sketchpixy/rubix';

import UpdateTodoMutation from '../mutations/UpdateTodoMutation';
import RemoveTodoMutation from '../mutations/RemoveTodoMutation';

@withRouter
class Todo extends React.Component {
  static contextTypes = {
    relay: Relay.PropTypes.Environment,
  };

  toggleCompletion() {
    this.context.relay.commitUpdate(
      new UpdateTodoMutation({
        todo: this.props.todo,
        completed: this.input.checked
      })
    );
  }

  removeTodo() {
    this.context.relay.commitUpdate(
      new RemoveTodoMutation({
        todo: this.props.todo,
        user: this.props.user
      })
    );
  }

  editTodo() {
    this.props.router.push(`/todo/edit/${this.props.todo.id}`);
  }

  render() {
    let { todo, completed } = this.props.todo;
    let style = {
      textDecoration: completed ? 'line-through' : null
    };

    return (
      <Grid>
        <Row className='todo-item'>
          <Col sm={8}>
            <Checkbox onChange={::this.toggleCompletion} style={style} inputRef={(input) => { this.input = input; }} checked={completed} >
              {todo}
            </Checkbox>
          </Col>
          <Col sm={4} className='text-right'>
            <Button bsStyle='red' className='remove-sm' onClick={::this.removeTodo} style={{marginRight: 12.5}}>Remove</Button>
            <Button bsStyle='green' className='remove-sm' onlyOnHover onClick={::this.editTodo}>Edit</Button>
          </Col>
        </Row>
      </Grid>
    );
  }
}

const TodoContainer = Relay.createContainer(Todo, {
  fragments: {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
        todo,
        completed,
        ${UpdateTodoMutation.getFragment('todo')},
        ${RemoveTodoMutation.getFragment('todo')}
      }
    `
  }
});

export default TodoContainer;

Let's breakdown the above code:

  • The above code defines a simple Todo component class. This component will be used in the index page where we render all todo items stored in our database.

  • We import all the necessary basic UI components from "@sketchpixy/rubix" for building our Todo component. To see all the different components that come bundled with Rubix please refer to the COMPONENTS section in the sidebar for complete documentation.

  • We also import the mutations (UpdateTodoMutation and RemoveTodoMutation) we created earlier.

  • Lines 24 - 31 defines a toggleCompletion() instance method that sets the completion state of the Todo item that was clicked. Since we are modifying the todo item, we dispatch a mutation new UpdateTodoMutation({ todo, completed }). The actual dispatch of the request to our backend is handled by Relay.

  • Lines 33 - 40 define a removeTodo() instance method. A call to this method removes the todo item. Since we are deleting the todo item, we dispatch an a mutation new RemoveTodoMutation({ todo, user }).

  • Lines 33 - 35 defines a editTodo() instance method. When this method is called, the router navigates to the edit page where the todo can be edited. We will cover editing in the upcoming section.

  • We finally render the Todo component with a Checkbox that controls the completion state along with two buttons to Remove and Edit the todo item.

  • However, the Todo component cannot be directly exported as a Route component. We need to create an intermediate Relay container by calling the Relay.createContainer method which also defines fragments that will be part of the final query. We then export the newly created TodoContainer as a Route component.

Step 6: Create relay-todolist/src/components/TodoForm.js

Let us now define a React component called TodoForm which handles creating new Todo items. Paste the following code into the newly created TodoForm.js file:


import React from 'react';
import Relay from 'react-relay';
import ReactDOM from 'react-dom';

import {
  Row,
  Col,
  Grid,
  Form,
  Alert,
  Button,
  Checkbox,
  FormGroup,
  FormControl } from '@sketchpixy/rubix';

import AddTodoMutation from '../mutations/AddTodoMutation';

export default class TodoForm extends React.Component {
  static contextTypes = {
    relay: Relay.PropTypes.Environment,
  };

  state = {
    errors: [],
  };

  createTodo(e) {
    e.preventDefault();

    let input = ReactDOM.findDOMNode(this.input);

    let todo = input.value;

    this.context.relay.commitUpdate(
      new AddTodoMutation({
        todo,
        user: this.props.user
      })
    , {
      onSuccess: () => {
        this.setState({
          errors: []
        });
      },
      onFailure: (transaction) => {
        let error = transaction.getError();
        let source = error.source;
        let message = error.message;
        if (source) {
          source = source.errors[0].message;

          this.setState({
            errors: source.split('\n')
          });
        } else {
          this.setState({
            errors: [message]
          })
        }
      }
    });

    input.value = '';
  }

  render() {
    let errors = this.state.errors.length ?
      (
        <Alert danger dismissible>
          {this.state.errors.map((error, i) => {
            return <div key={i}>{error}</div>
          })}
        </Alert>
      ) : null;

    return (
      <div>
        {errors}
        <Form horizontal onSubmit={::this.createTodo}>
          <FormGroup>
            <Col sm={10}>
              <FormControl type='text' placeholder='A todo item...' ref={(input) => this.input = input} autoFocus />
            </Col>
            <Col sm={2} collapseLeft>
              <br className='visible-xs' />
              <Button type='submit' bsStyle='blue' block onlyOnHover>Create Todo</Button>
            </Col>
          </FormGroup>
        </Form>
      </div>
    );
  }
}

The above code implements a TodoForm component that renders a form to create Todo items. It implements a createTodo method that is called when the form is submitted. The createTodo method dispatches an AddTodoMutation to create a new Todo item. We also define onFailure and onSuccess callbacks and display errors (if any) accordingly.

Now that we have created our basic components, it's time to create pages that render these components. This is detailed in the next section.