-->
ads here

GraphQL in practice - Part 2: Tạo ứng dụng GraphQL với Apollo Client và React

advertise here
Tiếp nối với Part 1, ở bài viết lần này mình sẽ tiếp tục trình bày part 2 của series: GraphQL in practice. Đây là bài viết thứ 2 và cũng là cuối cùng trong series này. Link của Part 1 các bạn có thể xem tại đây.
Trong phần này mình sẽ đi chi tiết về cách xây dựng UI của ứng dụng sử dụng Apollo Client và React JS. Nội dung của bài viết như sau:

  1. Giới thiệu - Setup môi trường
  2. Sử dụng Query để đọc data
  3. Sử dụng Mutation để thao tác với tập dữ liệu
  4. Make your application realtime by Subscription

1. Giới thiệu:

Mô tả:

Ứng dụng sẽ sử dụng server đã tạo ở part 1 để hiển thị danh sách các author, đồng thời có 1 form để tạo một author mới. Chúng ta sẽ sử dụng React để render dữ liệu và Apollo Client để connect với server và nhận dữ liệu.

Initialize Project:

  • Để tiết kiệm thời gian setup cho project mình sẽ sử dụng create-react-app package để khởi tạo project. Bạn có thể install create-react-app bằng cách 

 yarn add create-react-app  


  • Sau khi cài đặt create-react-app xong, chúng ta tiến hành khởi tạo project. Tạo 1 React app tên là client và điều hướng vào thư mục gốc của project bằng lệnh sau:

create-react-app client && cd client  



  • Khi đó ta sẽ được thư mục client như sau

client
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
Sau khi xong, chúng ta thử chạy lệnh sau để thấy kết quả ở http://localhost:3000

 npm run start  
 // OR  
 yarn start  

Setup Apollo Client

  • Trong thư mục src, ta tạo thư mục apollo để chứa các setup liên quan đến apollo. Tạo file apollo/client.js như sau:
1:  import { ApolloClient } from 'apollo-client';  
2:  import { InMemoryCache } from 'apollo-cache-inmemory';  
3:  import { createHttpLink } from 'apollo-link-http';  
4:  import { WebSocketLink } from 'apollo-link-ws';  
5:  import { split } from 'apollo-link';  
6:  import { getMainDefinition } from 'apollo-utilities';  
7:  const SERVER = 'http://localhost:4000/graphql';  
8:  const WS_SERVER = 'ws://localhost:4000/graphql';  
9:  const cache = new InMemoryCache();  
10:  const httpLink = new createHttpLink({  
11:   uri: SERVER,  
12:  });  
13:  export const wsLink = new WebSocketLink({  
14:   uri: WS_SERVER,  
15:   options: {  
16:    reconnect: true,  
17:    connectionParams: async () => {  
18:     return {};  
19:    },  
20:   },  
21:  });  
22:  const defaultOptions = {  
23:   query: {  
24:    fetchPolicy: 'network-only',  
25:   },  
26:  };  
27:  const terminatingLink = split(  
28:   ({ query }) => {  
29:    const { kind, operation } = getMainDefinition(query);  
30:    return (  
31:     kind === 'OperationDefinition' && operation === 'subscription'  
32:    );  
33:   },  
34:   wsLink,  
35:   httpLink,  
36:  );  
37:  const client = new ApolloClient({  
38:   link: terminatingLink,  
39:   cache,  
40:   defaultOptions,  
41:  });  
42:  export default client;  



  • SERVER và WS_SERVER là http url và web socket url trỏ tới server để đọc và ghi dữ liệu. SERVER được sử dụng trong httpLink để connect tới http server để nhận và ghi dữ liệu qua Query và Mutation còn WS_SERVER sử dụng trong wsLink để hiển thị những thay đổi của dữ liệu ở Client theo thời gian thực thông Subscription.
  • Tiếp theo, trong App.js, thêm đoạn code sau:

1:  import React, { Component } from 'react';  
2:  import { ApolloProvider } from 'react-apollo';  
3:  import client from './apollo/client';  
4:  import logo from './logo.svg';  
5:  import './App.css';  
6:  class App extends Component {  
7:   render() {  
8:    return (  
9:     <ApolloProvider client={client}>  
10:      <div className="App">  
11:        <header className="App-header">  
12:         <img src={logo} className="App-logo" alt="logo" />  
13:         <p>  
14:          Edit <code>src/App.js</code> and save to reload.  
15:         </p>  
16:         <a  
17:          className="App-link"  
18:          href="https://reactjs.org"  
19:          target="_blank"  
20:          rel="noopener noreferrer"  
21:         >  
22:          Learn React  
23:         </a>  
24:        </header>  
25:     </div>  
26:     </ApolloProvider>  
27:    );  
28:   }  
29:  }  
30:  export default App;



  • Start server và start client, mở http://localhost:3000 để thấy kết quả. Vậy là chúng ta đã xong phần khởi tạo project. Tiếp theo chúng ta sẽ render danh sách các author sử dụng bằng cách sử dụng Query

2. Sử dụng Query để đọc data

  • Để render danh sách các author, trước tiên ta sẽ tạo 1 Component tên là AuthorsList chứa danh sách các Author được trả về từ server. Tạo file components/AuthorsList.js như sau:
 import React from 'react';  
 import { Query } from 'react-apollo';  
 import gql from 'graphql-tag';  
 const AuthorQuery = gql`  
  query authors{  
   authors{  
    id  
    name  
    yearOfBirth  
    gender  
   }  
  }  
 `;  
 const AuthorsList = () => {  
  return (  
   <Query query={AuthorQuery}>  
    {({data, loading}) => {  
     if (loading) {  
      return (<div>Loading</div>)  
     }   
     return (<Authors authors={data.authors} />)  
    }}  
   </Query>  
  )  
 };  
 class Authors extends React.Component {   
  render() {  
   const { authors } = this.props;  
   return (  
    <ul>  
     {authors.map(author => (  
      <li key={author.id}>  
       <p>Author name: {author.name}</p>  
       <p>Year of Birth: {author.yearOfBirth}</p>  
       <p>Gender: {author.gender ? 'Male' : 'Female'}</p>  
      </li>  
     ))}  
    </ul>  
   )  
  }  
 }  
 export default AuthorsList;


  • Thông tin của tác giả sẽ được hiển thị bởi component Authors dưới dạng unordered list (ul), hiển thị các thông tin về tên, năm sinh và giới tính của tác giả
  • Tiếp theo ta sẽ define cú pháp Graphql để truy vấn dữ liệu cần thiết từ server, biến AuthorQuery sẽ chứa graphql-tag  chứa cú pháp Graphql.
  • Để xử lý dữ liệu trả về từ server ta sẽ dùng Component Query từ react-apollo . Query sẽ trả về 2 biến là loading và data để hiển thị dữ liệu.
    • Nếu đang load dữ liệu (loading = true) thì hiển thị dòng Loading,
    • Nếu dữ liệu đã loading xong (loading = false) thì sẽ render component Authors với dữ liệu từ data.authors
  • Sau khi tạo xong AuthorsList, ta sẽ import vào file App.js như sau: 
 import React, { Component } from 'react';  
 import { ApolloProvider } from 'react-apollo';  
 import client from './apollo/client';  
 import AuthorsList from './components/AuthorsList';  
 class App extends Component {  
  render() {  
   return (  
    <ApolloProvider client={client}>  
     <div className="App">  
      <AuthorsList />  
     </div>  
    </ApolloProvider>  
   );  
  }  
 }  
 export default App;  

Chạy http://localhost:3000 để thấy kết quả
Query result

3. Sử dụng Mutation để thao tác dữ liệu

  • Sau khi có list các author, chúng ta sẽ tạo 1 form để thêm 1 author mới. Form này sẽ được hiện thực trong component CreateAuthorForm.
  • Tạo 1 file components/CreateAuthorForm.js như sau:
 import React from 'react';  
 import {  
  compose,  
  withState,  
  withHandlers,  
 } from 'recompose';  
 import { graphql } from 'react-apollo';  
 import gql from 'graphql-tag';  
 const CreateAuthorMutation = gql`  
  mutation createAuthor(  
   $name: String!,  
   $yearOfBirth: Int!,  
   $gender: Boolean  
  ) {  
   createAuthor(  
    name: $name,  
    yearOfBirth: $yearOfBirth,  
    gender: $gender  
   ) {  
    id  
    name  
    yearOfBirth  
    gender  
   }  
  }  
 `;  
 const CreateAuthorForm = ({  
  onSubmit,  
  name,  
  yearOfBirth,  
  gender,  
  onChangeName,  
  onChangeYOB,  
  onChangeGender  
 }) => {  
  return (  
   <div>  
    <fieldset>  
     <legend><label htmlFor="name">Name:</label></legend>  
     <input type="text" id="name" value={name} onChange={e => onChangeName(e.target.value)} />  
    </fieldset>  
    <fieldset>  
     <legend><label htmlFor="yob">Year of Birth:</label></legend>  
     <input type="number" id="yob" value={yearOfBirth} onChange={e => onChangeYOB(e.target.value)} />  
    </fieldset>  
    <fieldset>  
     <legend><label htmlFor="gender">Gender:</label></legend>  
     <input type="checkbox" name="gender" checked={gender} onChange={e => onChangeGender(Boolean(e.target.checked))} /> Male  
    </fieldset>  
    <button onClick={() => onSubmit()}>Create Author</button>  
   </div>  
  )  
 }  
 const CreateAuthorFormContainer = compose(  
  graphql(CreateAuthorMutation, {name: 'createAuthor'}),  
  withState('name', 'setName', ''),  
  withState('yearOfBirth', 'setYOB', 1900),  
  withState('gender', 'setGender', true),  
  withHandlers({  
   onChangeName: ({ setName }) => val => setName(val),  
   onChangeYOB: ({ setYOB }) => val => setYOB(parseInt(val)),  
   onChangeGender: ({ setGender }) => val => setGender(val),  
   onSubmit: ({  
    createAuthor,  
    name,  
    yearOfBirth,  
    gender,  
   }) => async () => {  
    try {  
     await createAuthor({  
      variables: {  
       name, yearOfBirth, gender,  
      }  
     });  
    } catch(error) {  
     console.log(error);  
     alert(error.message);  
    }  
   },  
  }),  
 );  
 export default CreateAuthorFormContainer(CreateAuthorForm);  




  • Tương tự với Query, mình cũng có 1 biến để chứa cú pháp mutation để giao tiếp với server ở biến CreateAuthorMutation.
  • Tiếp theo mình sẽ hiện thực 1 form input trong CreateAuthorForm. Đây là một PureComponent có input là các props và trả form UI. Ở đây sẽ có bạn thắc mắc nếu mình không dùng class CreateAuthorForm extends React.Component mà dùng PureComponent thì làm sao để quản lý state cho các form input. Câu trả lời là mình sử dụng một package tên là recompose để làm việc này. Package này giúp mình tạo ra 1 High Order Component (HOC) wrap  CreateAuthorForm lại và truyền các state vào thông qua các props.
  • Cụ thể, mình tạo 1 HOC tên là CreateAuthorFormContainer và khai báo các props cần thiết trong đó. HOC này sẽ chứa các state cho các input như name, yearOfBirthgender đồng thời các handler xử lý khi các giá trị này thay đổi onChangeName, onChangeYOBonChangeGender. Cuối cùng chính là hàm onSubmit xử lý việc submit form.
  • Các bạn có thể thấy trong hàm onSubmit, mình sẽ gọi mutation và tạo 1 author sau khi user nhập dữ liệu và nhấn submit.

...  
 onSubmit: ({  
    createAuthor,  
    name,  
    yearOfBirth,  
    gender,  
   }) => async () => {  
    try {  
     await createAuthor({  
      variables: {  
       name, yearOfBirth, gender,  
      }  
     });  
    } catch(error) {  
     console.log(error);  
     alert(error.message);  
    }  
   },  
 ...  


  • Sau khi tạo CreateAuthorForm xong, chúng ta sẽ import nó và App.js:

 import React, { Component } from 'react';  
 import { ApolloProvider } from 'react-apollo';  
 import client from './apollo/client';  
 import AuthorsList from './components/AuthorsList';  
 import CreateAuthorForm from './components/CreateAuthorForm';  
 class App extends Component {  
  render() {  
   return (  
    <ApolloProvider client={client}>  
     <div className="App">  
      <CreateAuthorForm />  
      <AuthorsList />  
     </div>  
    </ApolloProvider>  
   );  
  }  
 }  
 export default App;  


  • Kiểm tra kết quả tại localhost
CreateAuthorForm
  • Lúc này bạn hãy thử tạo thêm 1 author mới. Sẽ không thấy thay đổi gì cho đến khi bạn F5 lại trình duyệt. Vậy câu hỏi là làm sao để có thể tạo author và ngay lập tức thấy nó được add vào trong list author bên dưới? Bây giờ chính là thời điểm nhập cuộc của Subscription. Hãy chú ý mục tiếp theo nhé.

4. Make your application realtime by Subscription

  • Quay trở lại file AuthorsList.js ta thêm biến CreateAuthorSubscription có nội dung như sau:
const CreateAuthorSubscription = gql`  
  subscription authorCreated {  
   authorCreated {  
    id  
    name  
    yearOfBirth  
    gender  
   }  
  }  
 `;  


  • Đây là cú pháp khai báo subscription sẽ được sử dụng. Ta có thể thấy rằng, subscription này được trigger khi createAuthor mutation được gọi và kết quả trả về của nó là thông tin của author mới được tạo.
  • Tiếp theo ta sửa 2 component AuthorsList và Author lần lượt như sau:

...  
 const AuthorsList = () => {  
  return (  
   <Query query={AuthorQuery}>  
    {({data, loading, subscribeToMore}) => { // Notice here  
     if (loading) {  
      return (<div>Loading</div>)  
     }  
     const more = () => subscribeToMore({ // And here  
      document: CreateAuthorSubscription,  
      updateQuery: (prev, { subscriptionData }) => {  
       if (!subscriptionData.data) return prev;  
       const newAuthor = subscriptionData.data.authorCreated;  
       return {  
        authors: [...prev.authors, newAuthor],  
       }  
      }  
     })  
     return (<Authors authors={data.authors} subscribeToNewAuthor={more} />) // And here  
    }}  
   </Query>  
  )  
 };  
 class Authors extends React.Component {  
  componentDidMount() {  
   this.props.subscribeToNewAuthor(); // And here  
  }  
  render() {  
   const { authors } = this.props;  
   return (  
    <ul>  
     {authors.map(author => (  
      <li key={author.id}>  
       <p>Author name: {author.name}</p>  
       <p>Year of Birth: {author.yearOfBirth}</p>  
       <p>Gender: {author.gender ? 'Male' : 'Female'}</p>  
      </li>  
     ))}  
    </ul>  
   )  
  }  
 }  
 ...  





  • Trong Query, chúng ta sẽ tạo thêm hàm more ở đây để xử lý data từ subscription. Nếu có dữ liệu từ subscription được truyền thông qua biến subscriptionData thì ta sẽ lấy dữ liệu đó (thông tin của author mới) và cập nhật vào danh sách author hiện tại. Khi đó sau mỗi lần tạo author mới thành công, ta sẽ thấy lập tức thông tin được hiển thị trong danh sách author hiện có. Ta truyền hàm more vào Authors component thông qua props mang tên subscribeToNewAuthor. Khi Authors component được render nó sẽ gọi hàm more để cập nhật dữ liệu nếu có.
  • Và đây là kết quả (Video nhìn cho nó real time :v )


Done, vậy là mình đã hoàn thành bài viết của chúng ta! Source code chi tiết mình có để trên github tại đây để các bạn có thể đọc và tham khảo cụ thể hơn.

Kết:

  • Mục đích bài viết này nói chung và series này nói riêng là giúp các bạn có cái nhìn căn bản nhất về cách sử dụng graphql ở server và cả client.
  • Trong bài viết này mình chỉ hiện thực 1 query, 1 mutation và 1 subscription cho các bạn hiểu. Nếu các bạn muốn thực hành thêm, bạn có thể hiện thực thêm tính năng update và delete author cũng như authorUpdated subscription để hiểu rõ hơn.
  • Đây là bài viết của tháng 12, bài viết đánh dấu mình đã hoàn thành mục tiêu mỗi tháng 1 bài viết của năm 2018. Hi vọng những bài viết của mình sẽ giúp ích cho các bạn, nếu các bạn thấy hay và bổ ích hãy chia sẻ cho mọi người cùng biết và nếu có ý kiến đóng góp xây dựng thì đừng ngần ngại để lại comment trong bất kì bài viết nào của mình nhé. Đó sẽ là những động lực để sang năm 2019 mình tăng số lượng bài viết cũng như chất lượng của từng bài gửi đến các bạn.
  • Cuối cùng, năm hết, Noel đến và Tết sắp về, mình chúc các bạn có một Giáng sinh an lành và năm mới tràn đầy thành công. Merry Xmas and Happy New Year!

Reference:

Advertisement
COMMENTS ()