03. Fetch and Submit Data with GraphQL
So far, our todo apps can insert custom name in profile page and can insert todo list. But this todo app is less helpful since we cannot track the progress of our todo items nor we can edit or delete it. Let's upgrade our todo apps! While upgrading it, we will also get closer into Treats GraphQL. Before starting this tutorial, you may want to read more about GraphQL. GraphQL is also not mandatory in Treats. If you want to disable it you can change it in treats.config.js
as instructed here.
GraphQL Configurations
Treats GraphQL configurations are under _graphql
folder. GraphQL hooks contains 3 files: link-state.js
, uri.js
, and config.js
. In this tutorial you need to deploy some GraphQL service. We recommend to use Graphcool to deploy your own GraphQL service.
- Deploy the following todo datamodel. For a quick start, you can use Graphcool console to create our schema.
type Todo {
id: ID! @isUnique
status: String
todoAction: String
}
You can get your graphql endpoint easily from the "endpoints" button in Graphcool console. The format will be like this:
https://api.graph.cool/simple/v1/<key>
- After getting your endpoint, create
_graphql
folder anduri.js
under the folder.
/* src/_graphql/uri.js */
const uri="https://api.graph.cool/simple/v1/<your_key>";
export default uri;
- Implement
config.js
under_graphql
folder. This config are based on Apollo configuration.
/* src/_graphql/config.js */
import uri from "./uri";
const customConfig = {
queryDeduplication: true,
link: [
{
type: "error",
callback: ({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
));
if (networkError) console.error(`[Network error]: ${networkError}`);
}
},
{
type: "batch-http",
uri: uri
}
]
};
export default customConfig;
- Of course, we need to declare the query and some mutation for our todo page let's create
todo
folder under_graphql
and createindex.js
,query.graphql
, andmutation.graphql
.
/* src/_graphql/index.js */
import * as todoQuery from "./query.graphql";
import * as todoMutation from "./mutation.graphql";
export { todoQuery };
export { todoMutation };
# src/_graphql/query.graphql
query GetAllTodoes {
allTodoes {
id,
todoAction,
status
}
}
# src/_graphql/mutation.graphql
mutation CreateTodo($todoAction: String!) {
createTodo(
todoAction: $todoAction
status: "not yet"
) {
id,
todoAction,
status
}
}
mutation DeleteTodo($id: ID!) {
deleteTodo(
id: $id
) {
id,
todoAction,
status
}
}
mutation UpdateTodo($id: ID!, $todoAction: String) {
updateTodo(
id: $id,
todoAction: $todoAction
) {
id,
todoAction
}
}
mutation UpdateTodoStatus($id: ID!, $status: String) {
updateTodo(
id: $id,
status: $status
) {
id,
status
}
}
- That's the GraphQL config we need for now. Read more in the GraphQL Client section.
Refactoring Todo List
Our current todo list still not connected to our GraphQL configuration we just created. Now it's time to refactor!
- Back to our
todo-list
first. Since our item now have state (done or pending) we can not show the item using literal string. We need to create new component for our todo items. Let's create one undercomponent
. You may use the component boilerplate generator.
/* src/component/todo-item/todo-item.js */
import React, { Component } from "react";
import style from "./todo-item.css";
class TodoItem extends Component {
render () {
const { id, todoAction, status } = this.props;
return (
<div >
<form
onSubmit={e => {
e.preventDefault();
}}
>
<input
ref={node => {
todo = node;
}}
defaultValue={todoAction}
readOnly={status === "done"}
/>
<span>
<button
className={style.button__orange}
type="submit"
>
Update Todo
</button>
<button
className={style.button__green}
>
Is it done?
</button>
</span>
<button
className={style.button__red}
>
Delete this todo
</button>
</form>
</div>
)
}
};
export default TodoItem;
/* src/component/todo-item/todo-item.css */
.button__green {
cursor: pointer;
background-color: #42b549;
color: #fff;
font-weight: 700;
margin: 10px;
}
.button__red {
cursor: pointer;
background-color: #d50000;
color: #fff;
font-weight: 700;
margin: 10px;
}
.button__orange {
cursor: pointer;
background-color: #f13700;
color: #fff;
font-weight: 700;
margin: 10px;
}
- As we can see, the item now contains buttons for update todo item, update status, and delete todo. To make our buttons do something, we need to connect it with mutation we defined earlier in
mutation.graphql
. To connect our components, we can use<Mutation>
component provided by Treats's GraphQL (Apollo) client.
Let's also create alias for _graphql
and component
, just like we've done on Redux.
// treats.config.js
const path = require("path");
const config = {
....
alias: {
"@page": path.resolve(__dirname, "./src/page"),
"@redux": path.resolve(__dirname, "./src/redux"),
"@graphql": path.resolve(__dirname, "./src/_graphql")
"@component": path.resolve(__dirname, "./src/component")
}
};
module.exports = config;
/* src/component/todo-item/todo-item.js */
import React, { Component } from "react";
import { Mutation } from "@treats/graphql";
import { todoMutation, todoQuery } from "@graphql/todo";
import style from "./todo-item.css";
class TodoItem extends Component {
render () {
const { id, todoAction, status } = this.props;
return (
<Mutation
mutation={todoMutation.DeleteTodo}
variables={{ id }}
refetchQueries={[{ query: todoQuery.GetAllTodoes }]}
>
{(deleteTodo, { loading: deleteLoading, error: deleteError }) => (
<Mutation
mutation={todoMutation.UpdateTodo}
variables={{ id, todoAction }}
refetchQueries={[{ query: todoQuery.GetAllTodoes }]}
>
{(updateTodo, { loading: updateLoading, error: updateError }) => (
<Mutation
mutation={todoMutation.UpdateTodoStatus}
variables={{ id, status: "done" }}
refetchQueries={[{ query: todoQuery.GetAllTodoes }]}
>
{(updateTodoStatus, { loading, error }) => {
let todo;
return (
<div >
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { todoAction: todo.value, id } });
}}
>
<input
ref={node => {
todo = node;
}}
defaultValue={todoAction}
readOnly={status === "done"}
/>
{status !== "done" && (
<span>
<button
className={style.button__orange}
type="submit"
>
Update Todo
</button>
<button
className={style.button__green}
onClick={updateTodoStatus}
>
Is it done?
</button>
</span>
)}
<button
className={style.button__red}
onClick={deleteTodo}
>
Delete this todo
</button>
</form>
{loading || (deleteLoading || updateLoading) && <p>Loading...</p>}
{error || (deleteError || updateError) && <p>Error :( Please try again</p>}
</div>
);
}}
</Mutation>
)}
</Mutation>
)}
</Mutation>
);
}
};
export default TodoItem;
Don't worry if it's long. If you read it carefully, we add every mutation into a single <Mutation>
component. That's why there are 3 nested Mutation out there. Also we add some network error handler like {loading || (deleteLoading || uploadLoading) ...}
and {error || (deleteError || updateError) ...}
. That makes our items contain 3 mutations updateTodo
, updateStatus
, and deleteTodo
.
- To make these mutation works, of course we also need to make the list stored in our GraphQL server. If it's stored in the server, then we need to fetch it when rendering our
todo-list
component. We're going to use<Query>
components to get our data from GraphQL server intodo-list.js
.
/* src/component/todo-list/todo-list.js */
import React from "react";
import { Query } from "@treats/graphql";
import { todoQuery } from "@graphql/todo";
import TodoItem from "../todo-item";
const TodoList = () => (
<Query
query={todoQuery.GetAllTodoes}
ssr={false}
fetchPolicy="cache-and-network"
>
{
({ loading, error, data }) => {
if (loading) return <p>Loading....</p>
if (error) return <p style={{"color": "red"}}>ERROR</p>
if (data.allTodoes) {
return (
<div>
{data.allTodoes.map(item => (
<TodoItem key={item.id} {...item} />
))}
</div>
);
}
return (
<div>
No Data
</div>
)
}
}
</Query>
);
export default TodoList;
- Yay! Now we can get data from GraphQL server. Wait a minute... But where the server get the data from?. We must create add
CreateTodo
mutation into our form intodo.js
.
/* src/page/todo/todo.js */
import React from "react";
import { FormattedMessage, injectIntl } from "@treats/intl";
import Link from "@treats/component/link";
import { connect } from "@treats/redux";
import { Mutation } from "@treats/graphql";
import { todoMutation, todoQuery } from "@graphql/todo";
import TodoList from "@component/todo-list";
import style from "./todo.css";
const Todo = ({ intl, name }) => (
<div>
...
<Mutation
mutation={todoMutation.CreateTodo}
refetchQueries={[{ query: todoQuery.GetAllTodoes }]}
>
{(addTodo, { loading, error }) => {
let input;
if (loading) {
return (<p>Loading...</p>)
}
if (error) {
return (<p>Error :( Please try again</p>)
}
return (
<div className={style.todo_form}>
<form onSubmit={e => {
e.preventDefault();
addTodo({ variables: { todoAction: input.value } });
input.value = "";
}}>
<input ref={
node => {
input = node;
}
} />
<button>{intl.formatMessage({id: "submit"})}</button>
</form>
</div>
)
}}
</Mutation>
<TodoList />
...
</div>
)
const mapStateToProps = state => ({
name: state.profile.name
})
export default connect(mapStateToProps)(injectIntl(Todo));
- All's done! Let's check our todo in GraphQL now! Pretty cool, isn't it? With some touch of css, now you can make this todo apps into more beatiful page.