loading...
Cover image for Fullstack with GraphQL, Prisma, Node, React and Apollo Boost

Fullstack with GraphQL, Prisma, Node, React and Apollo Boost

somprasongd profile image Somprasong Damyos ・13 min read

การพัฒนา Web Application ปกติ เราจะใช้ Architecture แบบ 3-tier Architecture

Alt Text

ซึ่ง API Server จะทำหน้าที่อยู่ 2 ส่วนหลักๆ คือ การทำ bussiness logic กับการติดต่อกับฐานข้อมูล ซึ่งการติดต่อกับฐานข้อมูลนั้นค่อนข้างวุ่นวาย มีทั้งการทำ ORM, Migration และเขียนคิวรี่ ซึ่งต้องเขียนให้ปลอดภัย และได้ performance ที่ดีอีก จึงมีแนวคิดที่จะแยกงานส่วนนี้ออกมาจก API Server เป็น Data Access Layer (DAL)

Alt Text

โดยตัว DAL เราจะใช้ Prisma มาจัดการ

Alt Text

ส่วน API Server จะติดต่อกับ Prisma Server ผ่านทาง Prisma client และตัว API Server ก็จะทำเป็น GraphQL Server โดย Architecture ทั้งหมดจะเป็นแบบนี้

Alt Text

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

Alt Text

Understand GraphQL Servers

  1. Definition: The GraphQL Schema
  2. Implementation: Resolver Functions
  3. 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

Alt Text

  • 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

    Alt Text

  • 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

Alt Text

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
  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 run npx create-react-app client

React Basic

  1. JSX
  • 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.js import '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;
      
  1. Components & Props
  • 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;
  1. State & Handling Events
  • 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;
    
  1. 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;
  1. Lifecycle

    Alt Text

  • 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;
  1. HTTP Request
  • 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

Alt Text

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
  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
  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 }
        });
      }
    });

Discussion

markdown guide