การพัฒนา Web Application ปกติ เราจะใช้ Architecture แบบ 3-tier Architecture
ซึ่ง API Server จะทำหน้าที่อยู่ 2 ส่วนหลักๆ คือ การทำ bussiness logic กับการติดต่อกับฐานข้อมูล ซึ่งการติดต่อกับฐานข้อมูลนั้นค่อนข้างวุ่นวาย มีทั้งการทำ ORM, Migration และเขียนคิวรี่ ซึ่งต้องเขียนให้ปลอดภัย และได้ performance ที่ดีอีก จึงมีแนวคิดที่จะแยกงานส่วนนี้ออกมาจก API Server เป็น Data Access Layer (DAL)
โดยตัว DAL เราจะใช้ Prisma มาจัดการ
ส่วน API Server จะติดต่อกับ Prisma Server ผ่านทาง Prisma client และตัว API Server ก็จะทำเป็น GraphQL Server โดย Architecture ทั้งหมดจะเป็นแบบนี้
GraphQL Introduction
What is GraphQL?
- New API standard developed by Facebook
- Specification for type system & query language
- Core primitives: Query, Mutation & Subscription
GraphQL Vs Rest
- Single Endpoint/Multiple Endpoints
- Client decides how data return/Server decides how data return
- Schemaful/Schemaless
How GraphQL Works
Understand GraphQL Servers
- Definition: The GraphQL Schema
- Implementation: Resolver Functions
- Setup: Framework, Network (HTTP), Middlewares
The GraphQL Schema
- Strongly typed & written in GraphQL Schema Definition Language (SDL)
- Defines API capabilities (contract for client-server communication)
- Used for: Auto-generated docs, codegen, automated tests, ...
- The Schema Definition Language (SDL)
type Task {
id: ID!
text: String!
completed: Boolean!
}
type Query {
tasks(search: String): [Task!]!
task(id: ID!): Task
}
type Mutation {
createTask(text: String!): Task!
toggleTask(id: ID!): Task
deleteTask(id: ID!): Task
}
Resolver Function
- Concrete implementation of the API
- One resolver function per field in SDL schema
- Query execution: Invoke resolvers for all fields in query
fieldName:(root, args, context, info) => { result }
- Example CRUD for Task type
const resolver = {
Query: {
tasks: (root, args, context, info) => context.db.getTasks(args.search),
task: (root, args, context, info) => context.db.getTask(args.id)
},
Mutation: {
createTask: (root, args, context, info) => context.db.createTask(args.text),
toggleTask:(root, args, context, info) => context.db.toggleStatus(args.id),
deleteTask: (root, args, context, info) => context.db.deleteTask(args.id)
}
};
Setup
- "GraphQL engine" to orchestrate resolver invocations
- Network layer based on "graphql-yoga" (network configuration: port, endpoints, CORS ... )
- Middleware (analytics, logging, crash reporting ... )
Put it all together
import { GraphQLServer } from 'graphql-yoga';
const typeDefs = `
type Query {
hello(name: String): String!
}
`;
const resolvers = {
Query: {
hello: (_, { name }) => `Hello ${name || 'World'}`,
},
};
const server = new GraphQLServer({ typeDefs, resolvers });
server.start(() => console.log('Server is running on localhost:4000'));
import { GraphQLServer } from 'graphql-yoga';
const tasks = [
{
id: 'task-1',
text: 'Learn GraphQL',
completed: false,
},
];
let nextTaskId = 2;
const typeDefs = `
type Task {
id: ID!
text: String!
completed: Boolean!
}
type Query {
tasks(search: String): [Task!]!
task(id: ID!): Task
}
type Mutation {
createTask(text: String!): Task!
toggleTask(id: ID!): Task
deleteTask(id: ID!): Task
}
`;
const resolvers = {
Query: {
tasks: (root, args, context, info) =>
args.search
? tasks.filter(task => task.text.toLocaleLowerCase().includes(args.search.toLocaleLowerCase()))
: tasks,
task: (root, args, context, info) => tasks.find(task => task.id === args.id),
},
Mutation: {
createTask: (root, args, context, info) => {
const newTask = {
// eslint-disable-next-line no-plusplus
id: `task-${nextTaskId++}`,
text: args.text,
completed: false,
};
tasks.push(newTask);
return newTask;
},
toggleTask: (root, args, context, info) => {
const taskToUpdate = tasks.find(task => task.id === args.id);
taskToUpdate.completed = !taskToUpdate.completed;
return taskToUpdate;
},
deleteTask: (root, args, context, info) => {
const deleteIndex = tasks.findIndex(task => task.id === args.id);
if (deleteIndex > -1) {
const deleted = tasks.splice(deleteIndex, 1);
return deleted[0];
}
return null;
},
},
};
const server = new GraphQLServer({ typeDefs, resolvers });
server.start(() => console.log('Server is running on localhost:4000'));
Refactor
- Use schema.graphql
type Task {
id: ID!
text: String!
completed: Boolean!
}
type Query {
tasks(search: String): [Task!]!
task(id: ID!): Task
}
type Mutation {
createTask(text: String!): Task!
toggleTask(id: ID!): Task
deleteTask(id: ID!): Task
}
- Create resolver module
// resolves/index.js
const tasks = [
{
id: 'task-1',
text: 'Learn GraphQL',
completed: false,
},
];
let nextTaskId = 2;
const resolvers = {
Query: {
tasks: (root, args, context, info) =>
args.search
? tasks.filter(task => task.text.toLocaleLowerCase().includes(args.search.toLocaleLowerCase()))
: tasks,
task: (root, args, context, info) => tasks.find(task => task.id === args.id),
},
Mutation: {
createTask: (root, args, context, info) => {
const newTask = {
// eslint-disable-next-line no-plusplus
id: `task-${nextTaskId++}`,
text: args.text,
completed: false,
};
tasks.push(newTask);
return newTask;
},
toggleTask: (root, args, context, info) => {
const taskToUpdate = tasks.find(task => task.id === args.id);
taskToUpdate.completed = !taskToUpdate.completed;
return taskToUpdate;
},
deleteTask: (root, args, context, info) => {
const deleteIndex = tasks.findIndex(task => task.id === args.id);
if (deleteIndex > -1) {
const deleted = tasks.splice(deleteIndex, 1);
return deleted[0];
}
return null;
},
},
};
export { resolvers };
- Update index.js
import { resolvers } from './resolvers';
// ...
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers,
});
// ...
Query & Mutation
- Query
query {
tasks {
id
text
completed
}
}
- Arguments
query {
tasks(search: "graphql") {
id
text
completed
}
}
- Mutation
mutation {
createTask(text: "Learn Prisma") {
id
text
}
}
- Operation Name
mutation createTask{
createTask(text: "Learn Prisma") {
id
text
}
}
query getAllTask{
tasks(search: "") {
id
text
completed
}
}
- Fragment
mutation createTask {
createTask(text: "Learn React") {
...taskFields
}
}
query getAllTask {
tasks(search: "") {
...taskFields
}
}
fragment taskFields on Task {
id
text
completed
}
- Alias
query getAllTask {
allTasks: tasks(search: "") {
...taskFields
}
}
fragment taskFields on Task {
taskId: id
text
completed
}
- Variable
query getAllTask($search: String) {
allTasks: tasks(search: $search) {
...taskFields
}
}
- Directive
query getAllTask($search: String, $withStatus: Boolean!) {
tasks(search: $search) {
id
text
completed @include(if: $withStatus)
}
}
Prisma Introduction
What is Prisma?
- DB-agnostic data access layer (think ORM)
- Prisma server & Prisma client
- Declarative data modelling & migrations
3-Main Concept
- Datamodel
- Prisma Server
- Prisma Client
Datamodel
- Defines the models of your application and is foundation for the Prisma client API.
- The datamodel is written in .prisma-files and uses GraphQL SDL syntax.
- Using the datamodel for database migrations (optional)
- datamodel.prisma
type Task {
id: ID! @id
text: String!
completed: Boolean! @default(value: false)
createdAt: DateTime! @createdAt
updatedAt: DateTime! @updatedAt
}
Prisma Server
- The Prisma server is a standalone infrastructure component that is connected to your database
- Prisma servers can be run with Docker
- docker-compose.yml
version: '3'
services:
prisma:
image: prismagraphql/prisma:1.34
restart: always
ports:
- "4466:4466"
environment:
PRISMA_CONFIG: |
port: 4466
# uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
# managementApiSecret: my-secret
databases:
default:
connector: postgres
host: postgres
user: prisma
password: prisma
rawAccess: true
port: 5432
migrations: true
postgres:
image: postgres
restart: always
# Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Postico
# ports:
# - "5432:5432"
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
- Prisma Services
- prisma.yml
endpoint: http://localhost:4466
datamodel: datamodel.prisma
Prisma Client
- The Prisma client is an auto-generated library that replaces a traditional ORM in your API
-
It connects to a Prisma server which sits on top your database
update prisma.yml
endpoint: http://localhost:4466
datamodel: datamodel.prisma
generate:
- generator: javascript-client
output: ./generated/prisma-client/
hooks:
post-deploy:
- prisma generate
- demo
// npm init -y && npm i prisma-client-lib
// playgroud.js
const {prisma} = require('./generated/prisma-client');
// A `main` function so that we can use async/await
async function main() {
// Create a new task
const newTask = await prisma.createTask({ text: 'Learn React' });
console.log(`Created new task: ${newTask.text} (ID: ${newTask.id})`);
// Read all tasks from the database and print them to the console
const allTasks = await prisma.tasks();
console.log('All Tasks:', allTasks);
// Fetch single task
const task = await prisma.task({ id: allTasks[0].id });
console.log('Task:', task);
// Filter task list
const filteredTask = await prisma.tasks({
where: {
text: task.text,
},
});
console.log('Filtered: ', filteredTask);
// Update task to completed
const updatedUser = await prisma.updateTask({
where: { id: task.id },
data: { completed: true },
});
console.log('Updated:', updatedUser);
// Delete task
const deletedTask = await prisma.deleteTask({ id: task.id });
console.log('Deleted:', deletedTask);
}
main().catch(e => console.error(e));
How Prisma Works
Add data access layer to GraphQL Server with Prisma
- Step 1: update prisma.yml
generate:
- generator: javascript-client
output: ../gql-server/src/generated/prisma-client/
- Step 2: Run
prisma generate
- Step 3: Run
npm i prisma-client-lib
- Step 4: Add database layer with Prisma
import { prisma } from './generated/prisma-client';
//...
const server = new GraphQLServer({
typeDefs,
resolvers,
context: {
prisma,
},
});
-
Step 5: Complete API operations against the database
- Create
resolvers/Query.js
- Create
const Query = {
tasks: (parent, { search }, { prisma }, info) =>
prisma.tasks({
where: {
text_contains: search,
},
}),
task: (parent, { id }, { prisma }, info) => prisma.task({ id }),
};
export { Query };
- Create
resolves/Mutation.js
const Mutation = {
createTask: (root, { text }, { prisma }, info) => prisma.createTask({ text }),
toggleTask: async (root, { id }, { prisma }, info) => {
const taskToUpdate = await prisma.task({ id });
return prisma.updateTask({
where: { id },
data: {
completed: !taskToUpdate.completed,
},
});
},
deleteTask: (root, { id }, { prisma }, info) => prisma.deleteTask({ id }),
};
export { Mutation };
- Update
resolves/index.js
import { Query } from './Query';
import { Mutation } from './Mutation';
const resolvers = {
Query,
Mutation,
};
export { resolvers };
Create Frontend App with React and Apollo Boost
Create React App
- Use
create-react-app
runnpx create-react-app client
React Basic
- JSX & CSS
import React from 'react';
import './App.css';
function App() {
const headerStyle = { color: 'green', fontSize: '50px' };
return (
<div>
<h1 style={headerStyle}>Todo App ({4})</h1>
<ul>
<li className="text">Learn React</li>
<li className="text">Learn React</li>
<li className="text">Learn React</li>
<li className="text">Learn React</li>
</ul>
</div>
);
}
export default App;
.text {
color: Tomato;
}
- Loop
import React from 'react';
import './App.css';
const todos = [
{
id: 1,
text: 'Learn React'
},
{
id: 2,
text: 'Learn GraphQL'
},
{
id: 3,
text: 'Learn Prisma'
}
];
function App() {
const headerStyle = { color: 'green', fontSize: '50px' };
return (
<div>
<h1 style={headerStyle}>Todo App ({todos.length})</h1>
<ul>
{todos.map(todo => (
<li className="text" key={todo.id}>
{todo.text}
</li>
))}
</ul>
</div>
);
}
export default App;
- Condition
import React from 'react';
import './App.css';
const todos = [
{
id: 1,
text: 'Learn React'
},
{
id: 2,
text: 'Learn GraphQL'
},
{
id: 3,
text: 'Learn Prisma'
}
];
// if-else or Ternary Operator or Logical && Operator
function App() {
const headerStyle = { color: 'green', fontSize: '50px' };
const count = todos.length;
return (
<div>
<h1 style={headerStyle}>Todo App ({todos.length})</h1>
{count === 0 ? (
<h3>All Done...</h3>
) : (
<ul>
{todos.map(todo => (
<li className="text" key={todo.id}>
{todo.text}
</li>
))}
</ul>
)}
</div>
);
}
export default App;
-
Use Semantic UI
-
Add Semantic UI React
- Components:
npm i semantic-ui-react
- Theme:
npm i semantic-ui-css
and import in index.jsimport 'semantic-ui-css/semantic.min.css'
import React from 'react'; import { Container, Segment } from 'semantic-ui-react'; import './App.css'; const todos = [ { id: 1, text: 'Learn React' }, { id: 2, text: 'Learn GraphQL' }, { id: 3, text: 'Learn Prisma' } ]; function App() { const headerStyle = { color: 'green', fontSize: '50px' }; const count = todos.length; return ( <Container> <h1 style={headerStyle}>Todo App ({todos.length})</h1> {count === 0 ? ( <h3>All Done...</h3> ) : ( <> {todos.map(todo => ( <Segment raised key={todo.id}> {todo.text} </Segment> ))} </> )} </Container> ); } export default App;
- Components:
-
-
TodoTitle.jsx
import React from 'react';
import { Icon, Header } from 'semantic-ui-react';
function TodoTitle(props) {
const count = props.count;
return (
<Header as="h2" color="teal" textAlign="left">
<Icon name="tasks" />
Todo App ({count})
</Header>
);
}
export default TodoTitle;
-
TodoItem.jsx
import React from 'react';
import { Segment } from 'semantic-ui-react';
function TodoItem(props) {
const { text } = props;
return <Segment raised>{text}</Segment>;
}
export default TodoItem;
-
Todo.jsx
import React from 'react';
import { Message, Loader, Grid } from 'semantic-ui-react';
import TodoTitle from './TodoTitle';
import TodoItem from './TodoItem';
const todos = [
{
id: 1,
text: 'Learn React'
},
{
id: 2,
text: 'Learn GraphQL'
},
{
id: 3,
text: 'Learn Prisma'
}
];
function Todo() {
const count = todos.length;
return (
<Grid
textAlign="center"
style={{ height: '100vh', marginTop: '25px' }}
verticalAlign="top"
>
<Grid.Column style={{ maxWidth: 600 }}>
<TodoTitle count={count} />
{loading && <Loader active inline="centered" />}
{!loading && count === 0 ? (
<Message success compact header="Hooray🎉 🎉 🎉 All Done 😆" />
) : (
<>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onDelete={handleRemoveTodo} />
))}
</>
)}
</Grid.Column>
</Grid>
);
}
export default Todo;
-
Counter App
- Class Component
import React from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
increment = () => {
this.setState({
count: (this.state.count += 1)
});
};
decrement = () => {
this.setState({
count: (this.state.count -= 1)
});
};
render() {
return (
<>
<h2>{this.state.count}</h2>
<div>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
</>
);
}
}
export default Counter;
- Hook
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<>
<h2>Count with Hook: {count}</h2>
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
</>
);
}
export default Counter;
- Todo App
- Add
import React, { useState } from 'react';
import TodoTitle from './TodoTitle';
import TodoItem from './TodoItem';
import { Button } from 'semantic-ui-react';
function Todo() {
const [todos, setTodos] = useState([]);
const handleAddTodo = () => {
const newTodo = {
id: todos.length + 1,
text: 'Test Add Todo'
};
setTodos([...todos, newTodo]);
};
const count = todos.length;
return (
<>
<TodoTitle count={count} />
<Button primary onClick={handleAddTodo}>
Add
</Button>
{count === 0 ? (
<h3>All Done...</h3>
) : (
<>
{todos.map(todo => (
<TodoItem key={todo.id} text={todo.text} />
))}
</>
)}
</>
);
}
export default Todo;
-
Delete
- Todo.jsx
import React, { useState, useRef } from 'react'; import { Message, Loader, Grid } from 'semantic-ui-react'; import TodoTitle from './TodoTitle'; import TodoItem from './TodoItem'; function Todo() { const [todos, setTodos] = useState([]); const nextId = useRef(1); const handleAddTodo = () => { const id = nextId.current++; const newTodo = { id, text: 'Test Add Todo: ' + id }; setTodos([...todos, newTodo]); }; const handleRemoveTodo = id => { console.log('Remove id:', id); // Filter all todos except the one to be removed const remainder = todos.filter(todo => todo.id !== id); // Update state with filter setTodos(remainder); }; const count = todos.length; return ( <Grid textAlign="center" style={{ height: '100vh', marginTop: '25px' }} verticalAlign="top" > <Grid.Column style={{ maxWidth: 600 }}> <TodoTitle count={count} /> <Button primary onClick={handleAddTodo}> Add </Button> {loading && <Loader active inline="centered" />} {!loading && count === 0 ? ( <Message success compact header="Hooray🎉 🎉 🎉 All Done 😆" /> ) : ( <> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onDelete={handleRemoveTodo} /> ))} </> )} </Grid.Column> </Grid> ); } export default Todo;
- TodoItem.jsx
import React from 'react'; import { Segment, Button } from 'semantic-ui-react'; function TodoItem(props) { const { todo: { id, text, completed = false }, onDelete } = props; return ( <> <Segment textAlign="left"> {text} <Button floated="right" style={{ marginTop: '-8px' }} icon="delete" color="red" onClick={() => { onDelete(id); }} /> </Segment> </> ); } export default TodoItem;
- Form
import React, { useState } from 'react';
import { Form, Message } from 'semantic-ui-react';
function TodoForm({ onSubmit }) {
const [text, setText] = useState('');
const [isWarning, setIsWarning] = useState(false);
const handleChange = e => {
setIsWarning(false);
setText(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
if (text === '') {
return setIsWarning(true);
}
onSubmit(text);
setText('');
};
return (
<Form warning onSubmit={handleSubmit}>
<Form.Field>
<Form.Input
placeholder="What needs to be done?"
name="text"
value={text}
onChange={handleChange}
/>
</Form.Field>
{isWarning && <Message warning header="Please input something!" />}
</Form>
);
}
export default TodoForm;
- Class Component
import React, { Component } from 'react';
class Clock extends Component {
state = { date: new Date() };
componentDidMount() {
console.log('componentDidMount');
this.timerID = setInterval(() => this.tick(), 1000);
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate');
}
componentWillUnmount() {
console.log('componentWillUnmount');
clearInterval(this.timerID);
}
tick() {
console.log('tick');
this.setState({
date: new Date()
});
}
render() {
console.log('render');
return (
<div>
<h2>Time is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
export default Clock;
- Hook
import React, { useState, useEffect } from 'react';
function Clock() {
const [date, setDate] = useState(new Date());
useEffect(() => {
const timerID = setInterval(() => tick(), 1000);
return () => {
clearInterval(timerID);
};
}, []);
const tick = () => {
setDate(new Date());
};
return (
<div>
<h2>Time (Hook) is {date.toLocaleTimeString()}.</h2>
</div>
);
}
export default Clock;
npm install axios
- Edit Todo.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Message, Loader, Grid } from 'semantic-ui-react';
import config from '../config';
import TodoTitle from './TodoTitle';
import TodoItem from './TodoItem';
import TodoForm from './TodoForm';
function Todo() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
// Make HTTP reques with Axios
axios.get(config.API_URL).then(res => {
// Set state with result
setTodos(res.data);
setLoading(false);
});
};
loadData();
}, []);
const handleAddTodo = text => {
// Assemble data
const newTodo = { text, isCompleted: false };
// Update data
axios.post(config.API_URL, newTodo).then(res => {
setTodos([...todos, res.data]);
});
};
const handleRemoveTodo = id => {
// Filter all todos except the one to be removed
const remainder = todos.filter(todo => todo.id !== id);
// Update state with filter
axios.delete(`${config.API_URL}/${id}`).then(res => {
setTodos(remainder);
});
};
const count = todos.length;
return (
<Grid
textAlign="center"
style={{ height: '100vh', marginTop: '25px' }}
verticalAlign="top"
>
<Grid.Column style={{ maxWidth: 600 }}>
<TodoTitle count={count} />
<TodoForm onSubmit={handleAddTodo} />
{loading && <Loader active inline="centered" />}
{!loading && count === 0 ? (
<Message success compact header="Hooray🎉 🎉 🎉 All Done 😆" />
) : (
<>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onDelete={handleRemoveTodo} />
))}
</>
)}
</Grid.Column>
</Grid>
);
}
export default Todo;
Connect your API to a client
How React connect to GraphQL Server
Set up Apollo Client
- Install packages
npm install apollo-boost @apollo/react-hooks graphql
apollo-boost
: Package containing everything you need to set up Apollo Client
@apollo/react-hooks
: React hooks based view layer integration
graphql
: Also parses your GraphQL queries
- Create Apollo Client in
index.js
import ApolloClient from 'apollo-boost';
const client = new ApolloClient({
uri: 'http://localhost:4000',
});
- Make your first query
import { gql } from 'apollo-boost';
// or you can use `import gql from 'graphql-tag';` instead
// ...
client
.query({
query: gql`
query fetchAllTask {
tasks {
id
text
completed
}
}
`
})
.then(result => console.log(result));
- Connect your client to React
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
const client = new ApolloClient({
uri: 'http://localhost:4000',
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
, document.getElementById('root'));
Query
- Request data in Component
- Create
gql/index.js
- Create
import { gql } from 'apollo-boost';
export default {
Query: {
getAllTodos: gql`
query fetchAllTasks {
todos: tasks {
id
text
completed
}
}
`
}
};
- Create
TodoList.jsx
import React from 'react';
import { Loader, Message } from 'semantic-ui-react';
import { useQuery } from '@apollo/react-hooks';
import gql from '../gql';
import ErrorMessage from './ErrorMessage';
import TodoItem from './TodoItem';
function TodoList() {
const { loading, error, data } = useQuery(gql.Query.getAllTodos);
if (loading) return <Loader active inline="centered" />;
if (error) return <ErrorMessage message={error.message} />;
const { todos } = data;
const count = todos.length;
const component =
count === 0 ? (
<Message success compact header="Hooray🎉 🎉 🎉 All Done 😆" />
) : (
<>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</>
);
return component;
}
export default TodoList;
- Update
TodoTitle.jsx
import React from 'react';
import { Icon, Header } from 'semantic-ui-react';
import { useQuery } from '@apollo/react-hooks';
import gql from '../gql';
function TodoTitle() {
const { loading, data } = useQuery(gql.Query.getAllTodos);
let count = 0;
if (!loading) count = data.todos.length;
return (
<Header as="h2" color="teal" textAlign="left">
<Icon name="tasks" />
Todo App ({count})
</Header>
);
}
export default TodoTitle;
Mutation
- Executing a mutation
- Update
gql/index.js
- Update
import { gql } from 'apollo-boost';
export default {
Query: {
getAllTodos: gql`
query fetchAllTasks {
todos: tasks {
id
text
completed
}
}
`
},
Mutation: {
addTodo: gql`
mutation addTask($text: String!) {
addTodo: createTask(text: $text) {
id
text
completed
}
}
`
}
};
- Update
TodoForm.jsx
import React, { useState } from 'react';
import { Form, Message } from 'semantic-ui-react';
import gql from '../gql';
import { useMutation } from '@apollo/react-hooks';
function TodoForm() {
const [text, setText] = useState('');
const [isWarning, setIsWarning] = useState(false);
const [addTodo, { data }] = useMutation(gql.Mutation.addTodo);
const handleChange = e => {
setIsWarning(false);
setText(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
if (text === '') {
return setIsWarning(true);
}
addTodo({ variables: { text } });
setText('');
};
return (
<Form warning onSubmit={handleSubmit}>
<Form.Field>
<Form.Input
placeholder="What needs to be done?"
name="text"
value={text}
onChange={handleChange}
/>
</Form.Field>
{isWarning && <Message warning header="Please input something!" />}
</Form>
);
}
export default TodoForm;
- Update
TodoItem.jsx
import React from 'react';
import { Segment, Button, Icon } from 'semantic-ui-react';
import { useMutation } from '@apollo/react-hooks';
import gql from '../gql';
function TodoItem(props) {
const {
todo: { id, text, completed = false }
} = props;
const [deleteTodo, { data }] = useMutation(gql.Mutation.deleteTodo);
const [toggleTodo] = useMutation(gql.Mutation.toggleTodo);
return (
<>
<Segment textAlign="left">
<Icon
color={completed ? 'teal' : 'grey'}
name="check circle"
size="large"
onClick={() => {
toggleTodo({
variables: {
id
}
});
}}
/>
{text}
<Button
floated="right"
style={{ marginTop: '-8px' }}
icon="delete"
color="red"
onClick={() => {
deleteTodo({
variables: {
id
}
});
}}
/>
</Segment>
</>
);
}
export default TodoItem;
- Updating the cache after a mutation
ถ้าการอัพเดท แล้ว response id กลับมาด้วย จะทำการอัพเดท cache ให้อัตโนมัติ เพราะ cache อ้างอิงจาก id
ส่วนการ create กับ delete ต้องไปอัพเดท cache เอง แล้วมันจะ render ใหม่
- Update
TodoForm.jsx
const [addTodo] = useMutation(gql.Mutation.addTodo, {
update(cache, { data: { addTodo } }) {
const { todos } = cache.readQuery({ query: gql.Query.getAllTodos });
cache.writeQuery({
query: gql.Query.getAllTodos,
data: { todos: [...todos, addTodo] }
});
}
});
- Update
TodoItem.jsx
const [deleteTodo] = useMutation(gql.Mutation.deleteTodo, {
update(cache, { data: { deleteTodo } }) {
const { todos } = cache.readQuery({ query: gql.Query.getAllTodos });
const remainder = todos.filter(todo => todo.id !== deleteTodo.id);
cache.writeQuery({
query: gql.Query.getAllTodos,
data: { todos: remainder }
});
}
});
Top comments (0)