# GraphQL 與 React Apollo
![image](https://miro.medium.com/max/1400/1*_WLQgOYacSMectxlmkOjpg.png)
# (一) 引言
提到 GraphQL 與 React Apollo ,這個在 Web 前端被廣泛討論的技術名詞,究竟他們兩個是甚麼呢?
由於這兩個技術名詞,會涉及前後端的串接 [註一],在介紹這兩個技術名詞之前,先提一下以往在串接前後端的做法。時至 2020 年的今日,姑且稱呼在這年之前的歷史發展,都叫「以往」吧。
## 早期: XMLHttpRequest()
範例比如[這裡](https://gist.github.com/EtienneR/2f3ab345df502bd3d13e),在 JavaScript Promise 語法出來之前,使用這種方式非同步的從後端取得資料,前端可以再搭配 callback 處理這些非同步的資料。
## 中期: Fetch API
遇到非同步程式碼,若只能使用 Callback 往往不利閱讀與維護。
在 JavaScript Promise 語法出來後,官方推出 [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch),前端可使用 chain 的方式,發送 HTTP request 取得資料。另外有部分開發者仍覺得有些不好用的地方,開發了 [Axios](https://github.com/axios/axios)、[SuperAgent](https://visionmedia.github.io/superagent/) 等函式庫。以下文章統稱為 Fetch API。
## 近期: 2013 年 Facebook 首次推出 React 後
為了能搭配 React 這個 UI 函式庫,更方便使用 Fetch API,開發者們會將不同的階段,如等待結果中、資料正確取回、發生錯誤等等,用不同 React State 記錄,再自行封裝在 class component,或是 Hook 中,方便使用。
## 最近:2015 年 Redux 推出 react-redux
為了讓 React 更好的管理並記錄狀態,在 React 推出 Context API 前,Redux 團隊先推出了 react-redux,讓 React 透過 Redux 的狀態管理,能跨 Component 間分享狀態。
這時,開發人員又覺得,我們別將取資料跟 Component 混雜在一起,把非同步取資料的程式碼統一寫在進 Reducer 之前,再讓 Reducer 寫入資料到 Store,不是很好嗎?[註二]
在這樣的概念下,不同團隊各開發了不同的 Middleware 比如 redux-thunk, redux-saga, redux-observable,用來處理非同步的程式碼。
其中 [redux-observable](https://redux-observable.js.org/) 紅極一時,可以讓 Redux 搭配 RxJS 使用,而 RxJS 號稱是非同步程式碼的 Lodash [註三]。
## But ...
寫起來實在是很多行,以 redux-observable 為例,要寫 Action, Action-Creator, Fetch API, Epic, Reducer 才能達成串接並寫入資料 Store,寫起來行數其實挺多的。
redux-observable 能針對特殊的狀況,比如[取消網路請求](https://www.youtube.com/watch?v=sF5-V-Szo0c&feature=youtu.be&t=566),能漂亮且有效處理。大部分情況下,取個資料顯示在前端,或是使用者操作,送個資料回後端,其實並不複雜。
有沒有辦法,在大部分的情況下讓程式碼行數減少,更有效率的開發呢?
# (二) 介紹
## 什麼是 GraphQL?
從[官方](https://graphql.org/)的定義來看
"GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data."
1. GraphQL 是**新的查詢語言**,為了詢問 API 而生。
2. GraphQL 是能**與既存資料互動的執行環境**,為了滿足新的查詢語言。
## 新的查詢語言 — 使用宣告式語法
GraphQL 提供宣告式的、類似物件的語法,得到自己想查詢的資料
如下例,左邊是我們宣告的內容,描述希望取到什麼資料,右邊是 server 回傳值。關於語法如何使用,可參考[這](https://graphql.org/learn/)
![image](https://miro.medium.com/max/1400/1*luMee6NG9va4RkvrjSo5fw.png)
## 與既存資料互動的執行環境
GraphQL 提供函式庫,讓我們能完成支援 GraphQL 查詢語法的 server。這個 server 常被當成是中介的 server:前端先與 GraphQL server 取資料, GraphQL server 再發送 HTTP request 向資料庫取資料,這種做法也被稱為 "GraphQL as an API Gateway"。
GraphQL server 經由函式庫將來源資料,轉換成帶上原型的 instance 後,前端就能使用上述宣告式的語法,取值或是送值回 server。
關於如何使用 Node Express 架出 GraphQL server 、如何轉換來源資料,可參考[這個 Demo Repo](https://github.com/andy770921/graphQL-demo) 或是[這個 Demo Repo](https://github.com/iamshaunjp/graphql-playlist/tree/lesson-36),server 資料夾下的程式碼。將靜態的 JavaScript 陣列 [註四],轉成可供 GraphQL 宣告式語法查詢的資料。
## 什麼是 React Apollo?
從[官方](https://github.com/apollographql/react-apollo)的定義來看
"React Apollo allows you to fetch data from your GraphQL server and use it in building complex and reactive UIs using the React framework."
React Apollo 是**在 React 框架下,可以讓我們從 GraphQL server 取資料的函式庫**,官方提供了可以適用不同情境的 React Hook ,讓我們可以依照不同情境使用。
## 為什麼要用 GraphQL / React Apollo?
GraphQL / React Apollo 主要解決了三大問題。
### 一、串接後端從多個 Endpoint 變一個
以書籍、作者的頁面當例子,如果希望畫面呈現某本書及詳細的細節,及推薦作者寫的其他書,在使用 REST API 時,我們可能會先發送請求,用某書籍 ID,到書籍路徑下的 Endpoint ,取到書籍詳細資料與作者。
```domain.com/book/:id```
等資料回來後,再用作者 ID 發送請求到另一個作者路徑下的 Endpoint ,取到該作者著作的所有書籍。
```domain.com/author/:id```
如果同個頁面,又要顯示顯示所有書籍的資料,我們又必須要再打以下 API。
```domain.com/books```
如果使用 GraphQL / React Apollo,我們只需要打同一個 Endpoint 就可以取得所有的資料。
```domain.com/graphql```
依照不同查詢情境,query string 會被自動處理好,取得我們想要的資料。
### 二、前端精準取得自己要的資料
依照使用情境,取的自己需要的資料即可,可以少取可以多取(巢狀取)。
![image](https://miro.medium.com/max/1400/1*uyhbUbEGhANWwQMfIIpwFA.png)
### 三、處理非同步的複雜邏輯,不需要自己處理
相對於 redux-observable 的規範,要寫 Action, Action-Creator, Fetch API, Epic, Reducer 等等,React Apollo 只需要依照情境選用 Hook,程式碼量降低,在使用情境單純的情況,較易理解。
![image](https://miro.medium.com/max/1400/1*fe3_i4oEuQj2WqJm2-zi_A.png)
# (三) 實作
## 前置準備 — GraphQL Server
如果要串接的後端,是第三方的服務,並且已經提供支援 GraphQL 的 Endpoint,如串接 [GitHub](https://developer.github.com/v4/)、[GitLab](https://docs.gitlab.com/ee/api/graphql/)
如果需要自己寫,可以透過 NodeJS 配合套件 express-graphql 實作,程式碼可簡單寫如下範例
![image](https://miro.medium.com/max/944/1*ayEX4dK756vHmQvjHFZzoQ.png)
schema 是從官方提供的函式庫 ```new GraphQLSchema()``` 產生出物件實例 ( instance ),可以依照自己需求,在 GraphQLSchema 帶入自定義的物件來設定,自行客製可供串接的 data 項目, schema 完整範例可參考[這裡的程式碼](https://github.com/andy770921/graphQL-demo/blob/master/server/schema/schema.js)
我們在上圖程式碼第 14 行有設定,```graphiql``` 為 ```true```,意思是前端可以直接在瀏覽器輸入網址如 ```http://localhost:4000/graphql``` ,連到 GraphQL 提供的網頁介面
![image](https://miro.medium.com/max/1400/1*zUne3rjkBTZS7aOnRi0iHQ.png)
可以先測試看看查詢語法是否正確、有哪些資料可以取、是否真的可以取得的資料等等
## 使用 React Apollo 三步驟
### 一、建立 Apollo Client
建立 Apollo Client 需要使用 Apollo 官方的 ```ApolloProvider``` React Component,它是封裝完 Context Provider 後的 React Component [註五],所以只需要在 Component 外層,用 ```<ApolloProvider>``` 包起來如下
```js
<ApolloProvider>
<ChildComponent />
</ApolloProvider>
```
裡面的 ```<ChildComponent />``` 就可以取用 React Apollo 查詢回來的資料
```<ApolloProvider>``` 需帶入 props 名稱為 client、值為 ```new ApolloClient()``` 創造出的物件實例,GraphQL 的 Endpoint 可在創出時,代入物件寫在裡面,如下
```js
import React from 'react';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
const client = new ApolloClient(
{ uri: 'http://localhost:4000/graphql' }
);
const App = () => (
<ApolloProvider client={client}>
<Child />
</ApolloProvider>
)
```
建立完 Apollo Client 後,就完成了第一步
### 二、寫 GraphQL 的專用查詢語法
看要拿甚麼資料就怎麼寫,如取得所有書籍的名稱及 ID,寫法可如下,詳細的語法使用可參考[這裡](https://graphql.org/learn/)
```js
import { gql } from 'apollo-boost';
const GET_BOOKS_QUERY = gql`
{
books {
id
name
}
}
`;
```
依照需要的資料寫好查詢語法後,就完成了第二步
### 三、依情境使用 Hooks
**情境一:進入頁面就取資料 — useQuery**
![image](https://miro.medium.com/max/1400/1*lrcS5Qnlzk5fEtYJJQwPLg.png)
使用 ```useQuery``` 後,會立刻回傳一個物件。在未來某一個時間點,```useQuery``` 回傳會回傳新的物件,引發 react component 再渲染一次。
物件的內容,可使用 JS 解構賦值的方式,取出內部的狀態 key,對應到的值,比如可以取出 ```loading``` ,這個狀態是布林值。
首次渲染畫面的時候,```useQuery``` 的狀態分別是:```{ loading: true, error: undefined, data: undefined }``` 代表的意義是,```useQuery``` 已發送請求給 graphQL server,並在等待資料回來,在 loading 中。
未來某一個時間點,資料回來或是收到錯誤,會回傳新的物件,進而讓 react-hook 再次渲染畫面。若正確收到資料,```useQuery``` 的狀態分別是:```{ loading: false, error: undefined, data: dataObject{...} }``` 代表的意義是,```useQuery``` 已不在 loading 中,並可讓我們取到 data 物件。若收到錯誤,```useQuery``` 的狀態分別是:```{ loading: false, error: errObject{...} , data: undefined }```,可針對錯誤進行處理。
以下 ```console.log``` 的結果可幫助理解,共發生兩次渲染。
![image](https://miro.medium.com/max/1400/1*LRBWKOVWjTt536zSzbTY7g.png)
**情境二:使用者操作才取資料 — useLazyQuery**
![image](https://miro.medium.com/max/1400/1*H95JcgWDe7DPezzmVO9k5w.png)
使用 ```useLazyQuery``` 後,會立刻回傳一個陣列。未來若依不同情境變更了狀態,```useLazyQuery``` 會回傳新的陣列,引發 react component 再渲染。
陣列的內容,可使用 JS 陣列解構賦值的方式,重新命名陣列元素名稱,陣列的第一個元素為函數、第二個元素為物件。因為[陣列解構賦值](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)的特性,不論取什麼名稱,功能不變。如可以取名為 ```[getBookQuery, bookObj]```,也可取為 ```[bananaFn, bananaObject]```。
可再將陣列取出來的第二個元素,再解構賦值,如
```js
const [getBookQuery, { loading, error, data }] = useLazyQuery(GET_BOOK_QUERY,{
variables: {
id: selectedBookId
}
});
```
首次渲染畫面的時候,上述 loading, error, data 分別為 ```false, undefined, undefined```
當呼叫陣列第一個元素回傳的函式 ```getBookQuery``` 時,開始發送請求給 graphQL server,這時上述 loading, error, data 更新為 ```true, undefined, undefined```
當收到資料或是收到錯誤時,上述 loading, error, data 更新為 ```false, undefined, dataObject{...}``` 或 ```false, errObject{...}, undefined```
以下範例 ```console.log``` 可幫助理解,一進入發生畫面發生了一次渲染,在使用者選完下拉選單後,再發生兩次渲染
![image](https://miro.medium.com/max/1400/1*qboPGjUtvWLWl4zqBnFncg.png)
**情境三:使用者輸入後,傳資料給 server — useMutation**
![image](https://miro.medium.com/max/1400/1*549ZOgdOI8M8BbVhhwihww.png)
使用 ```useMutation``` 後,會立刻回傳一個陣列。未來若依不同情境變更了狀態, ```useMutation``` 會回傳新的陣列,引發 react component 再渲染。
陣列的內容,可使用 JS 陣列解構賦值的方式取出,像 ```useLazyQuery``` 一樣。
這個陣列的第一個元素是函式。觸發這個函式,就會開始發送請求給 graphQL server。
這個陣列的第二個元素是物件。可知道各個時間點的 loading, error, data 狀態
經過上述的 Hooks 使用,可以滿足大部分的操作情境。
# (四) 結語
時至 2020 年 6 月的今天,如果採用 GraphQL 及 React Apollo,可能有以下的優點
1. GraphQL 可做為 API Gateway ,讓前端更方便、更精準的操作資料
2. React Apollo 可滿足大部分資料操作情境,比如向 server 取資料、再取更多資料、修改資料等等
3. React Apollo 也幫我們在 React 的架構下,使用 Hook 及 Context API 完成了資料的存放、錯誤狀態的處理等等,實際上用起來,程式碼簡單不少。
希望可以加速各位開發速度,有機會的話試用看看吧
# 註解:
註一:即 server 的資料,經過網路傳輸後,由瀏覽器頁面顯示。文章所提的前後端的串接,指後端(Server) 傳給前端(Client)一個 JSON 格式的資料,這個資料再被前端解析後取用,又稱 Client Side Render
註二:詳細的原因及 Redux 官方對 Middleware 的說明[在此](https://redux.js.org/advanced/middleware)
註三: *RxJS is sometimes described as "LoDash for asynchronous events."* --- from [Angular2 Pocket Primer](https://books.google.com.tw/books?id=4E0qDwAAQBAJ&pg=SA8-PA28&lpg=SA8-PA28&dq=rxjs+async+lodash&source=bl&ots=fLd17EwjLO&sig=ACfU3U0NyqfxEqL_JZ1VFOXKLqVnfbaRSQ&hl=zh-TW&sa=X&ved=2ahUKEwjej9f6nazpAhURCqYKHVVQDSIQ6AEwAnoECAkQAQ#v=onepage&q=rxjs%20async%20lodash&f=false)
註四:來源資料陣列範例如下
![image](https://miro.medium.com/max/1178/1*NWbei2lGrcDt4IMWBq8hng.png)
註五:實際封裝的狀況,可以在瀏覽器下載 React 開發人員套件,使用開發人員工具看出,被封裝了三層。在最內層的 Context.Provider 內的某個 props 節點下,有取回來的 data
![image](https://miro.medium.com/max/1400/1*nhb3rAn9iSlSLt2UFt62uQ.png)