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.
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
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
}
`,
}
});
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} />
);
}
}
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 connectionedges
, 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')}
}
}
}
`
},
});
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')}
},
}
`,
},
});
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} />
);
}
}
We have now defined our components and containers. In browser, you should be able to see the app with todos we created in GraphiQL.
In the next section we will define Relay mutations to update our todos.