Reindex
TeamBlogDocs

Displaying todos

Now that we have a schema, we can display the Todos in our client application.

Let’s install some dependencies, like libraries to handle keycodes and classnames and css for TodoMVC.

npm install --save classnames keycodes todomvc-app-css

Let’s create components Todo.js, TodoInput.js, TodoList.js and TodoApp.js.

Running the app

Let’s start the app so that we can see it’s current state in the browser. At this point it will show the default reindex-starter-kit-react view.

First fetch the schema for Relay. You need to fetch it every time you push the schema to the Reindex backend.

reindex schema-relay data/schema.json

Then start the app:

npm start

Todo.js

The biggest difference between a ‘plain’ React app and React + Relay app is that every component you create is wrapped into a container. Container contains a query fragment, that describes what data a component needs. For example for a Todo, we need text and whether it is completed, as well its ID. So the fragment would be:

fragment on Todo {
  id,
  text,
  complete
}

You wrap the component into container by providing this fragment to a fragment object. The key of that fragment would be passed to the component props.

export default Relay.createContainer(Todo, {
  fragments: {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
        text,
        complete
      }
    `
  }
});

Here is full src/components/Todo.js file. We leave stubs in place of future mutative actions.

import React, {Component} from 'react';
import Relay from 'react-relay';
import classNames from 'classnames';

import TodoInput from './TodoInput';

class Todo extends Component {
  state = {
    isEditing: false,
  }

  handleCompleteChange = () => {
    // TODO: handle complete
  }

  handleLabelDoubleClick = () => {
    this.setState({
      isEditing: true,
    });
  }

  handleDestroyClick = () => {
    // TODO: handle destroy
  }

  handleInputSave = (text) => {
    // TODO: handle text change
    this.setState({
      isEditing: false,
    });
  }

  handleInputCancel = () => {
    this.setState({
      isEditing: false,
    });
  }

  handleInputDelete = () => {
    this.setState({
      isEditing: false,
    });
  }

  makeInput() {
    if (this.state.isEditing) {
      return (
        <TodoInput className="edit"
                   saveOnBlur={true}
                   initialValue={this.props.todo.text}
                   onSave={this.handleInputSave}
                   onCancel={this.handleInputCancel}
                   onDelete={this.handleInputDelete} />
      );
    } else {
      return null;
    }
  }

  render() {
    return (
      <li className={classNames({
        completed: this.props.todo.complete,
        editing: this.state.isEditing
      })}>
        <div className="view">
          <input checked={this.props.todo.complete}
                 className="toggle"
                 onChange={this.handleCompleteChange}
                 type="checkbox" />
           <label onDoubleClick={this.handleLabelDoubleClick}>
             {this.props.todo.text}
           </label>
          <button className="destroy"
                  onClick={this.handleDestroyClick} />
        </div>
        {this.makeInput()}
      </li>
    );
  }
}

export default Relay.createContainer(Todo, {
  fragments: {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
        text,
        complete
      }
    `,
  }
});

TodoInput.js

Here is src/components/TodoInput.js, an input component. It’s “dumb” component, without any Relay containers.

import keycodes from 'keycodes';
import React, {Component} from 'react';
import {findDOMNode} from 'react-dom';

export default class TodoInput extends Component {
  state = {
    text: this.props.initialValue || '',
  };

  handleBlur = () => {
    if (this.props.saveOnBlur) {
      this.save();
    }
  }

  handleChange = (e) => {
    this.setState({
      text: e.target.value,
    });
  }

  handleKeyDown = (e) => {
    if (e.keyCode === keycodes('esc')) {
      if (this.props.onCancel) {
        this.props.onCancel();
      }
    } else if (e.keyCode === keycodes('enter')) {
      this.save();
    }
  }

  save() {
    const text = this.state.text.trim();
    if (text === '') {
      if (this.props.onDelete) {
        this.props.onDelete();
      }
    } else if (text === this.props.initialValue) {
      if (this.props.onCancel) {
        this.props.onCancel();
      }
    } else {
      if (this.props.onSave) {
        this.props.onSave(text);
      }
      this.setState({
        text: '',
      });
    }
  }

  componentDidMount() {
    findDOMNode(this).focus();
  }

  render() {
    return (
      <input className={this.props.className || ''}
             placeholder={this.props.placeholder || ''}
             value={this.state.text}
             onBlur={this.handleBlur}
             onChange={this.handleChange}
             onKeyDown={this.handleKeyDown} />
    );
  }
}

TodoList.js

Connection is Relay’s convention for representing lists of data. Relay knows how to use connections for, for example, optimistic updates or pagination.

Reindex creates connection types automatically for all types with the interface Node (“node types”). The name of such connection type for is _<name-of-type>Connection, so for Todo it is _TodoConnection.

The TodoList component will display multiple todos, so the fragment needs to be defined on _TodoConnection.

The connection type has two important fields:

  • count, the total number of nodes in the connection
  • edges, a list of items, with the actual node (todo in this case) accessible using the node key.
fragment on _TodoConnection {
  count,
  edges {
    node {
      complete,
      ${Todo.getFragment('todo')}
    }
  }
}

With the ${} syntax we can include requirements of the child components, in this case Todo in our parent query. Note how we still have added complete to the query, even though it is there from Todo - this is because we enable filtering based on the completion inside TodoList and Relay only passed component the data it directly requested in props.

Here is full listing of src/components/TodoList.js

import React, {Component} from 'react';
import Relay from 'react-relay';

import Todo from './Todo';

class TodoList extends Component {
  getFilteredTodos() {
    const edges = this.props.todos.edges;
    if (this.props.filter === 'active') {
      return edges.filter((todo) => !todo.node.complete);
    } else if (this.props.filter === 'completed') {
      return edges.filter((todo) => todo.node.complete);
    } else {
      return edges;
    }
  }

  handleToggleAllChange = () => {
    // TODO: handle toggle all
  }

  makeTodo = (edge) => {
    return (
      <Todo key={edge.node.id}
            todo={edge.node}
            viewer={this.props.viewer} />
    );
  }

  render() {
    const todoCount = this.props.todos.count;
    const done = this.props.todos.edges.reduce((next, edge) => (
      next + (edge.node.complete ? 1 : 0)
    ), 0);
    const todos = this.getFilteredTodos();
    const todoList = todos.map(this.makeTodo);
    return (
      <section className="main">
        <input className="toggle-all"
               checked={todoCount === done}
               onChange={this.handleToggleAllChange}
               type="checkbox" />
        <ul className="todo-list">
          {todoList}
        </ul>
      </section>
    );
  }
}

export default Relay.createContainer(TodoList, {
  fragments: {
    todos: () => Relay.QL`
      fragment on _TodoConnection {
        count,
        edges {
          node {
            complete,
            ${Todo.getFragment('todo')}
          }
        }
      }
    `
  },
});

TodoApp.js

As we discussed before, to get all the Todos we will use allTodos field on from viewer root. viewer type is ReindexViewer, so our TodoApp container will define fragment on it.

fragment on ReindexViewer {
  allTodos(first: 1000000) {
    count,
    edges {
      node {
        complete
      }
    }
    ${TodoList.getFragment('todos')}
  },

Again, we include some more data manually, because we need information about completion in the App component to display it in the footer.

first argument needs to be passed to all connections in Relay, so that it can handle pagination. We don’t plan to implement pagination in our TodoApp, but we still have to pass some arbitrary big number for Relay’s sake. This limitation will be removed in future version of Relay. The common pattern in Relay is to use Number.MAX_SAFE_INTEGER for this, but Reindex only supports 32 bits integers as arguments to first and last.

Full code listing for src/components/TodoApp.js.

import React, {Component} from 'react';
import Relay from 'react-relay';
import classNames from 'classnames';

import TodoList from './TodoList';
import TodoInput from './TodoInput';

import 'todomvc-app-css/index.css';

class TodoApp extends Component {
  state = {
    selectedFilter: 'all',
  };

  handleFilterChange = (filter) => {
    this.setState({
      selectedFilter: filter,
    });
  }

  handleInputSave = (text) => {
    // TODO: handle save
  }

  handleClearCompleted = () => {
    // TODO: handle clear completed
  };

  makeHeader() {
    return (
      <header className="header">
        <h1>Todos</h1>
        <TodoInput className="new-todo"
                   placeholder="What needs to be done?"
                   onSave={this.handleInputSave} />
      </header>
    );
  }

  makeFooter() {
    const total = this.props.viewer.allTodos.count;
    const undone = this.props.viewer.allTodos.edges.reduce((next, edge) => (
      next + (edge.node.complete ? 0 : 1)
    ), 0);

    const filters = ['all', 'active', 'completed'].map((filter) => {
      const selected = filter === this.state.selectedFilter;
      return (
        <li key={filter}>
          <a href={'#' + filter}
             className={classNames({ selected })}
             onClick={selected ? null : this.handleFilterChange.bind(
               this, filter
             )}>
             {filter}
          </a>
        </li>
      );
    })

    let clearButton;
    if (this.props.viewer.allTodos.edges.some((edge) => edge.node.complete)) {
      clearButton = (
        <button className="clear-completed"
                onClick={this.handleClearCompleted}>
          Clear completed
        </button>
      );
    }

    return (
      <footer className="footer">
        <span className="todo-count">
          {undone} / {total} items left
        </span>
        <ul className="filters">
          {filters}
        </ul>
        {clearButton}
      </footer>
    );
  }

  render() {
    return (
      <section className="todoapp">
        {this.makeHeader()}
        <TodoList todos={this.props.viewer.allTodos}
                  filter={this.state.selectedFilter}
                  viewer={this.props.viewer} />
        {this.makeFooter()}
      </section>
    );
  }
}

export default Relay.createContainer(TodoApp, {
  fragments: {
    viewer: () => Relay.QL`
      fragment on ReindexViewer {
        allTodos(first: 1000000) {
          count,
          edges {
            node {
              id,
              complete
            }
          }
          ${TodoList.getFragment('todos')}
        },
      }
    `,
  },
});

Routes and App.js

Last thing to do is to fix our Route and App.js. Routes in Relay define entry points in the application, in our example the entry point would be viewer.

Let’s create src/routes/AppRoute.js

import Relay from 'react-relay';

export default class AppRoute extends Relay.Route {
  static queries = {
    viewer: () => Relay.QL`query { viewer }`,
  };
  static routeName = 'AppRoute';
}

Let’s hook it all up in src/components/App.js

import React, {Component} from 'react';
import Relay from 'react-relay';

import Reindex from '../Reindex';
import TodoApp from './TodoApp';
import AppRoute from '../routes/AppRoute';

export default class App extends Component {
  render() {
    return (
      <Relay.RootContainer
        Component={TodoApp}
        route={new AppRoute}
        forceFetch={true} />
    );
  }
}

Run the app

We have now defined our components and containers. In browser, you should be able to see the app with todos we created in GraphiQL.

Screenshot

In the next section we will define Relay mutations to update our todos.

Topics:

Displaying todos

Now that we have a schema, we can display the Todos in our client application.

Let’s install some dependencies, like libraries to handle keycodes and classnames and css for TodoMVC.

npm install --save classnames keycodes todomvc-app-css

Let’s create components Todo.js, TodoInput.js, TodoList.js and TodoApp.js.

Running the app

Let’s start the app so that we can see it’s current state in the browser. At this point it will show the default reindex-starter-kit-react view.

First fetch the schema for Relay. You need to fetch it every time you push the schema to the Reindex backend.

reindex schema-relay data/schema.json

Then start the app:

npm start

Todo.js

The biggest difference between a ‘plain’ React app and React + Relay app is that every component you create is wrapped into a container. Container contains a query fragment, that describes what data a component needs. For example for a Todo, we need text and whether it is completed, as well its ID. So the fragment would be:

fragment on Todo {
  id,
  text,
  complete
}

You wrap the component into container by providing this fragment to a fragment object. The key of that fragment would be passed to the component props.

export default Relay.createContainer(Todo, {
  fragments: {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
        text,
        complete
      }
    `
  }
});

Here is full src/components/Todo.js file. We leave stubs in place of future mutative actions.

import React, {Component} from 'react';
import Relay from 'react-relay';
import classNames from 'classnames';

import TodoInput from './TodoInput';

class Todo extends Component {
  state = {
    isEditing: false,
  }

  handleCompleteChange = () => {
    // TODO: handle complete
  }

  handleLabelDoubleClick = () => {
    this.setState({
      isEditing: true,
    });
  }

  handleDestroyClick = () => {
    // TODO: handle destroy
  }

  handleInputSave = (text) => {
    // TODO: handle text change
    this.setState({
      isEditing: false,
    });
  }

  handleInputCancel = () => {
    this.setState({
      isEditing: false,
    });
  }

  handleInputDelete = () => {
    this.setState({
      isEditing: false,
    });
  }

  makeInput() {
    if (this.state.isEditing) {
      return (
        <TodoInput className="edit"
                   saveOnBlur={true}
                   initialValue={this.props.todo.text}
                   onSave={this.handleInputSave}
                   onCancel={this.handleInputCancel}
                   onDelete={this.handleInputDelete} />
      );
    } else {
      return null;
    }
  }

  render() {
    return (
      <li className={classNames({
        completed: this.props.todo.complete,
        editing: this.state.isEditing
      })}>
        <div className="view">
          <input checked={this.props.todo.complete}
                 className="toggle"
                 onChange={this.handleCompleteChange}
                 type="checkbox" />
           <label onDoubleClick={this.handleLabelDoubleClick}>
             {this.props.todo.text}
           </label>
          <button className="destroy"
                  onClick={this.handleDestroyClick} />
        </div>
        {this.makeInput()}
      </li>
    );
  }
}

export default Relay.createContainer(Todo, {
  fragments: {
    todo: () => Relay.QL`
      fragment on Todo {
        id,
        text,
        complete
      }
    `,
  }
});

TodoInput.js

Here is src/components/TodoInput.js, an input component. It’s “dumb” component, without any Relay containers.

import keycodes from 'keycodes';
import React, {Component} from 'react';
import {findDOMNode} from 'react-dom';

export default class TodoInput extends Component {
  state = {
    text: this.props.initialValue || '',
  };

  handleBlur = () => {
    if (this.props.saveOnBlur) {
      this.save();
    }
  }

  handleChange = (e) => {
    this.setState({
      text: e.target.value,
    });
  }

  handleKeyDown = (e) => {
    if (e.keyCode === keycodes('esc')) {
      if (this.props.onCancel) {
        this.props.onCancel();
      }
    } else if (e.keyCode === keycodes('enter')) {
      this.save();
    }
  }

  save() {
    const text = this.state.text.trim();
    if (text === '') {
      if (this.props.onDelete) {
        this.props.onDelete();
      }
    } else if (text === this.props.initialValue) {
      if (this.props.onCancel) {
        this.props.onCancel();
      }
    } else {
      if (this.props.onSave) {
        this.props.onSave(text);
      }
      this.setState({
        text: '',
      });
    }
  }

  componentDidMount() {
    findDOMNode(this).focus();
  }

  render() {
    return (
      <input className={this.props.className || ''}
             placeholder={this.props.placeholder || ''}
             value={this.state.text}
             onBlur={this.handleBlur}
             onChange={this.handleChange}
             onKeyDown={this.handleKeyDown} />
    );
  }
}

TodoList.js

Connection is Relay’s convention for representing lists of data. Relay knows how to use connections for, for example, optimistic updates or pagination.

Reindex creates connection types automatically for all types with the interface Node (“node types”). The name of such connection type for is _<name-of-type>Connection, so for Todo it is _TodoConnection.

The TodoList component will display multiple todos, so the fragment needs to be defined on _TodoConnection.

The connection type has two important fields:

  • count, the total number of nodes in the connection
  • edges, a list of items, with the actual node (todo in this case) accessible using the node key.
fragment on _TodoConnection {
  count,
  edges {
    node {
      complete,
      ${Todo.getFragment('todo')}
    }
  }
}

With the ${} syntax we can include requirements of the child components, in this case Todo in our parent query. Note how we still have added complete to the query, even though it is there from Todo - this is because we enable filtering based on the completion inside TodoList and Relay only passed component the data it directly requested in props.

Here is full listing of src/components/TodoList.js

import React, {Component} from 'react';
import Relay from 'react-relay';

import Todo from './Todo';

class TodoList extends Component {
  getFilteredTodos() {
    const edges = this.props.todos.edges;
    if (this.props.filter === 'active') {
      return edges.filter((todo) => !todo.node.complete);
    } else if (this.props.filter === 'completed') {
      return edges.filter((todo) => todo.node.complete);
    } else {
      return edges;
    }
  }

  handleToggleAllChange = () => {
    // TODO: handle toggle all
  }

  makeTodo = (edge) => {
    return (
      <Todo key={edge.node.id}
            todo={edge.node}
            viewer={this.props.viewer} />
    );
  }

  render() {
    const todoCount = this.props.todos.count;
    const done = this.props.todos.edges.reduce((next, edge) => (
      next + (edge.node.complete ? 1 : 0)
    ), 0);
    const todos = this.getFilteredTodos();
    const todoList = todos.map(this.makeTodo);
    return (
      <section className="main">
        <input className="toggle-all"
               checked={todoCount === done}
               onChange={this.handleToggleAllChange}
               type="checkbox" />
        <ul className="todo-list">
          {todoList}
        </ul>
      </section>
    );
  }
}

export default Relay.createContainer(TodoList, {
  fragments: {
    todos: () => Relay.QL`
      fragment on _TodoConnection {
        count,
        edges {
          node {
            complete,
            ${Todo.getFragment('todo')}
          }
        }
      }
    `
  },
});

TodoApp.js

As we discussed before, to get all the Todos we will use allTodos field on from viewer root. viewer type is ReindexViewer, so our TodoApp container will define fragment on it.

fragment on ReindexViewer {
  allTodos(first: 1000000) {
    count,
    edges {
      node {
        complete
      }
    }
    ${TodoList.getFragment('todos')}
  },

Again, we include some more data manually, because we need information about completion in the App component to display it in the footer.

first argument needs to be passed to all connections in Relay, so that it can handle pagination. We don’t plan to implement pagination in our TodoApp, but we still have to pass some arbitrary big number for Relay’s sake. This limitation will be removed in future version of Relay. The common pattern in Relay is to use Number.MAX_SAFE_INTEGER for this, but Reindex only supports 32 bits integers as arguments to first and last.

Full code listing for src/components/TodoApp.js.

import React, {Component} from 'react';
import Relay from 'react-relay';
import classNames from 'classnames';

import TodoList from './TodoList';
import TodoInput from './TodoInput';

import 'todomvc-app-css/index.css';

class TodoApp extends Component {
  state = {
    selectedFilter: 'all',
  };

  handleFilterChange = (filter) => {
    this.setState({
      selectedFilter: filter,
    });
  }

  handleInputSave = (text) => {
    // TODO: handle save
  }

  handleClearCompleted = () => {
    // TODO: handle clear completed
  };

  makeHeader() {
    return (
      <header className="header">
        <h1>Todos</h1>
        <TodoInput className="new-todo"
                   placeholder="What needs to be done?"
                   onSave={this.handleInputSave} />
      </header>
    );
  }

  makeFooter() {
    const total = this.props.viewer.allTodos.count;
    const undone = this.props.viewer.allTodos.edges.reduce((next, edge) => (
      next + (edge.node.complete ? 0 : 1)
    ), 0);

    const filters = ['all', 'active', 'completed'].map((filter) => {
      const selected = filter === this.state.selectedFilter;
      return (
        <li key={filter}>
          <a href={'#' + filter}
             className={classNames({ selected })}
             onClick={selected ? null : this.handleFilterChange.bind(
               this, filter
             )}>
             {filter}
          </a>
        </li>
      );
    })

    let clearButton;
    if (this.props.viewer.allTodos.edges.some((edge) => edge.node.complete)) {
      clearButton = (
        <button className="clear-completed"
                onClick={this.handleClearCompleted}>
          Clear completed
        </button>
      );
    }

    return (
      <footer className="footer">
        <span className="todo-count">
          {undone} / {total} items left
        </span>
        <ul className="filters">
          {filters}
        </ul>
        {clearButton}
      </footer>
    );
  }

  render() {
    return (
      <section className="todoapp">
        {this.makeHeader()}
        <TodoList todos={this.props.viewer.allTodos}
                  filter={this.state.selectedFilter}
                  viewer={this.props.viewer} />
        {this.makeFooter()}
      </section>
    );
  }
}

export default Relay.createContainer(TodoApp, {
  fragments: {
    viewer: () => Relay.QL`
      fragment on ReindexViewer {
        allTodos(first: 1000000) {
          count,
          edges {
            node {
              id,
              complete
            }
          }
          ${TodoList.getFragment('todos')}
        },
      }
    `,
  },
});

Routes and App.js

Last thing to do is to fix our Route and App.js. Routes in Relay define entry points in the application, in our example the entry point would be viewer.

Let’s create src/routes/AppRoute.js

import Relay from 'react-relay';

export default class AppRoute extends Relay.Route {
  static queries = {
    viewer: () => Relay.QL`query { viewer }`,
  };
  static routeName = 'AppRoute';
}

Let’s hook it all up in src/components/App.js

import React, {Component} from 'react';
import Relay from 'react-relay';

import Reindex from '../Reindex';
import TodoApp from './TodoApp';
import AppRoute from '../routes/AppRoute';

export default class App extends Component {
  render() {
    return (
      <Relay.RootContainer
        Component={TodoApp}
        route={new AppRoute}
        forceFetch={true} />
    );
  }
}

Run the app

We have now defined our components and containers. In browser, you should be able to see the app with todos we created in GraphiQL.

Screenshot

In the next section we will define Relay mutations to update our todos.