+6

Mini Apollo GraphQL React client - server

Chào anh em, lại là mình đây 😄. Hôm nay mình lại tiếp tục đi đến một chủ đề khá thú vị, cực kì phù hợp cho các anh em thích nghịch ngợm và cũng đơn giản để có thiển triển khai các pet project trong tương lai. Cụ thể là vọc vạch về một ngôn ngữ truy vấn gọi là GraphQL và nó cũng đã hỗ trợ rất nhiều thư viện chính chủ cho cả 2 phía client-server nên rất tiện, trong phần này mình sẽ sử dụng nó và kết hợp với React nhé. Nào cũng chơi thôi 😆.

1. Chuẩn bị

Yêu cầu:

  • Đã có một chút kiến thức về GraphQL cũng như React.
  • Môi trường mình sẽ sử dụng:
    • window 10
    • node v12.14.0
    • yarn v1.22.4
    • editor: VSCode

Mục đích:

  • Tìm hiểu xem cách sử dụng.
  • Hỗ trợ cho việc xây dựng các pet project trong tương lai.

Những phần bỏ qua:

  • Phần cấu hình mình sẽ không mô tả chi tiết trong bài viết, các bạn có thể theo dõi thông qua repo.
  • Không giải thích các thuật ngữ, khái niệm cơ bản mà các bạn hoàn toàn có thể đọc thông qua trang chính thức của thư viện đó.

2. Nội dung

GraphQL là gì ?

Về cơ bản thì graphql là một ngôn ngữ truy vấn và các bạn có thể mường tượng nó một cách tương đương RESTful khi viết phần api cho project.

Ở bài viết này mình sẽ không đi giải thích cụ thể, các bạn có thể tìm hiểu đầy đủ thông qua tài liệu chính thức tại đây.

React là gì ?

Các bạn đã đọc bài này thì chắc chắn đã hiểu biết về React vì vậy hãy đến phần tiếp theo nhé 😆

Xây dựng GraphQL phía server

Mình sẽ dùng Apollo GraphQL các bạn có thể xem qua tại đây

  1. Cài đặt thư viện
yarn add apollo-server graphql

apollo-server server cho express. graphql dành cho js.

  1. Tạo một database đơn giản
const data = [
  {
    id: 1,
    title: 'Sach 1',
    author: 'Dai 1',
    createdAt: new Date().toISOString(),
  },
  {
    id: 2,
    title: 'Sach 2',
    author: 'Dai 2',
    createdAt: new Date().toISOString(),
  },
]
  1. Tạo một GraphQL schema
const { gql } = require('apollo-server')

const typeDefs = gql`
  type Book {
    id: ID!
    title: String!
    author: String!
    createdAt: String!
  }

  type Query {
    books: [Book]
  }
`

Book sẽ được dùng như một shape, một kiểu dữ liệu và mô tả về cấu trúc của đối tượng đó khi nó được truy suất đến. books: [Book] mô tả khi ta truy vấn đến books và sẽ response cho ta 1 mảng Book, nó tương đương như khi ta gọi GET /books bên RESTful

  1. Tạo một resolver
const resolvers = {
  Query: {
    books: () => data,
  },
}

Một bản đồ các hàm trả về dữ liệu khi ta query tương ứng với schema mô tả ở trên

  1. Tạo GraphQL server
const { ApolloServer } = require('apollo-server')

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => console.log(`Server ready at ${url}`));
  1. Run server

Mặc định GraphQL server sẽ dùng port 4000

  1. Test

Mọi thứ chỉ đơn giản như vậy 😄

Xây dựng phía client và request đến GraphQL server

  1. Cài đặt thư viện
yarn add @apollo/client graphql

React và các thư viện build, compiler các bạn có thể xem qua repo nhé.

  1. Tạo kết nối với phía server
import { ApolloClient, InMemoryCache } from '@apollo/client'

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
})

uri của server khi nãy chúng ta dựng. cache cơ chế cache ta muốn cho các truy vấn GraphQL phía client.

  1. Tạo một lệnh truy vấn
import { gql } from '@apollo/client'

const GET_BOOKS = gql`
  query {
    books {
      id
      title
      author
      createdAt
    }
  }
`;

Chúng ta sẽ dùng đến kiểu cú pháp template string trong es6 và bên trong mô tả những gì ta muốn truy vấn, bạn sẽ thấy nó giống khi nãy chúng ta demo trên http://localhost:4000/

  1. Tạo một component để request cũng như hiển thị data sau khi truy vấn
import React from 'react'
import { useQuery } from '@apollo/client'

function App() {
  const { loading, error, data } = useQuery(GET_BOOKS)

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error {error.message}</p>

  return (
    <div>
      {data.books.map((book) => (
        <div key={book.id} style={{ borderBottom: '1px solid gray' }}>
          <h3>Title: {book.title}</h3>
          <p>Author {book.author}</p>
          <span>{book.createdAt}</span>
        </div>
      ))}
    </div>
  )
}
  1. Kết nối App và apollo thông qua context
import { ApolloProvider } from '@apollo/client'

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('app'),
)
  1. Run app
yarn webpack serve
  1. Kết quả

Quá ngon, đó là tất cả những gì chúng ta cần 😄.

Truy vấn để thêm dự liệu

Tất nhiên ngoài về lấy dữ liêu chúng ta còn cần phải thêm, sửa, xóa. Chúng ta cùng xem qua phần add book sau.

  1. Phía server
const typeDefs = gql`
  type Book {
    id: ID!
    title: String!
    author: String!
    createdAt: String!
  }

  type Query {
    books: [Book]
  }

+  input AddBookInput {
+   title: String!
+   author: String!
+  }

+  type Mutation {
+    addBook(input: AddBookInput!): Book
+  }
`

Khai báo tham số đầu vào khi request đến addBook và kết quả khi tạo thành công sẽ là record book vừa được tạo.

const resolvers = {
  Query: {
    books: () => data,
  },
+  Mutation: {
+    addBook(parent, args, context, info) {
+      const { title, author } = args.input
+      const newBook = {
+        id: Date.now(),
+        title,
+        author,
+        createdAt: new Date().toISOString(),
+      }

+      data.push(newBook)

+      return newBook
+    },
+  },
}

Chúng ta sẽ tạo mới với chính xác shape của Book và trả về chính nó cho client.

  1. Phía client

Thêm một truy vấn cho phần thêm mới book

import { gql } from '@apollo/client'

const ADD_BOOK = gql`
  mutation($input: AddBookInput!) {
    addBook(input: $input) {
      id
      title
      author
      createdAt
    }
  }
`

Tạo một form đơn giản

import React, { useRef } from 'react'
import { useMutation } from '@apollo/client'

function AddBookForm() {
  const titleRef = useRef('')
  const authorRef = useRef('')

  const [addBook] = useMutation(ADD_BOOK)

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()

        addBook({
          variables: {
            input: {
              title: titleRef.current.value,
              author: authorRef.current.value,
            },
          },
        })
      }}
    >
      <h3>Add book form</h3>
      <div>
        <label htmlFor="title">title</label>
        <input id="title" type="text" ref={titleRef} />
      </div>
      <div>
        <label htmlFor="author">author</label>
        <input id="author" type="text" ref={authorRef} />
      </div>
      <button type="submit">Add book</button>
    </form>
  )
}
function App() {
  const { loading, error, data } = useQuery(GET_BOOKS)

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error {error.message}</p>

  return (
+    <div>
+      <AddBookForm />
+      <br />
      <div>
        {data.books.map((book) => (
          <div key={book.id} style={{ borderBottom: '1px solid gray' }}>
            <h3>Title: {book.title}</h3>
            <p>Author {book.author}</p>
            <span>{book.createdAt}</span>
          </div>
        ))}
      </div>
+    </div>
  )
}

Test xem nhé

Mọi thứ có vẻ đã làm việc đúng như mong đợi.

Ơ nhưng khoan, khi thêm mới thì ta cần hiển thị realtime dữ liệu vừa thêm đấy tại local ? Tất nhiên

Apollo GraphQL sẽ lưu dữ liệu khi truy vấn xong vào cache để những khi component re-render sẽ không bị request lại. Vì vậy khi tạo mới thành công cũng ta cũng sẽ tiến hành sửa đổi trong cache để Apollo cập nhật data lại cho query GET_BOOKS, tất nhiên bạn có thể buộc request lại bằng 1 option khác nhưng ở đây mình sẽ cập nhật cache nhé.

function AddBookForm() {
  const titleRef = useRef('')
  const authorRef = useRef('')

+  const resetInput = () => {
+    titleRef.current.value = ''
+    authorRef.current.value = ''
+  }

  const [addBook] = useMutation(ADD_BOOK, {
+   update(cache, { data }) {
+     // Fetch the books from the cache
+     const existingBooks = cache.readQuery({ query: GET_BOOKS })

+     // Add the new book to the cache
+     const newBook = data.addBook
+
+      cache.writeQuery({
+        query: GET_BOOKS,
+        data: { books: [newBook, ...existingBooks.books] },
+      })
+    },
+    onCompleted: resetInput,
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()

        addBook({
          variables: {
            input: {
              title: titleRef.current.value,
              author: authorRef.current.value,
            },
          },
        })
      }}
    >
      <h3>Add book form</h3>
      <div>
        <label htmlFor="title">title</label>
        <input id="title" type="text" ref={titleRef} />
      </div>
      <div>
        <label htmlFor="author">author</label>
        <input id="author" type="text" ref={authorRef} />
      </div>
      <button type="submit">Add book</button>
    </form>
  )
}

Ngoài ra mình sẽ clear toàn bộ input sau khi thêm mới thành công và dùng nó thông qua callback onCompleted

Và như bạn thấy đấy mọi thứ là hoàn toàn ngon lành như mong đợi 😄.

3. Kết luận

Mọi thứ trông thật đơn giản bạn nhỉ 😆.

Ngoài ra sẽ còn rất nhiều tính năng thú vị khác, nếu bạn cảm thấy thích thú hãy tìm hiểu nhiều hơn qua trang chính thức của GraphQLApollo GraphQL nhé.

Hi vọng bài viết này có ích và giúp bạn trong quá trình tiến tới hoàn thiện khả năng lập trình Web hơn nhé 💯.

Cảm ơn đã đọc bài viết này 👏 Repo tại đây


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí