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