---
# System prepended metadata

title: GraphQL 與 React Apollo

---

# 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)