Apollo Client 入門指南:現代 React App 的 GraphQL 最佳搭檔

更新日期: 2025 年 6 月 10 日

GraphQL 雖然帶來了靈活的資料查詢方式,但在實務開發中,前端開發者仍然會遇到不少難題。

例如:

  • 如何方便地送出查詢與變更?
  • 如何有效管理 loading、error 與資料狀態?
  • 如何避免重複請求,實作快取與 UI 同步?
  • 如何讓本地資料與伺服器資料保持一致?

這些挑戰需要一套穩定且一致的資料管理機制,而 Apollo Client 就是在這樣的需求下誕生

它不僅能幫助你與 GraphQL 伺服器溝通,更內建快取邏輯、自動處理錯誤與狀態,讓你專注於前端介面的開發。

在 Apollo Client 出現之前:我們怎麼使用 GraphQL?

雖然 GraphQL 提供了比 REST 更靈活的資料查詢能力,但在它初期被採用的階段,前端開發者並沒有像 Apollo Client 這樣的完整工具可用。

因此,在開發者眼中,GraphQL 雖然「語法漂亮」、「查詢自由」,但實際操作起來,卻像一把雙面刃:該自己寫的還是得自己寫、能踩的坑一個也不缺

這裡,我們將一步步還原當時開發流程與常見問題,讓你清楚看見 Apollo Client 解決了哪些痛點。

手動撰寫 HTTP 請求:使用 fetch 或 Axios 發送查詢

GraphQL 使用單一端點(如 /graphql)搭配 POST 請求,查詢內容通常是字串型別嵌入 JSON 格式的 body 中。

這讓我們不得不每次都寫類似以下的程式碼:

fetch('https://api.example.com/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`, // 有時還得自己處理 token
  },
  body: JSON.stringify({
    query: `
      query GetUsers($limit: Int!) {
        users(limit: $limit) {
          id
          name
        }
      }
    `,
    variables: {
      limit: 10,
    }
  }),
})
  .then(res => res.json())
  .then(result => {
    if (result.errors) {
      console.error(result.errors);
    } else {
      console.log(result.data);
    }
  });

問題是什麼?

  • 查詢寫成模板字串,沒有語法提示、沒有型別檢查。
  • 查詢欄位拼錯要等 runtime 才知道。
  • 每次查詢都要手動管理變數與 header,錯誤容易發生。
  • 資料結構與元件耦合緊密,不易共用與維護。

手動管理請求狀態與 UI:loading、error、data 自行切換

GraphQL 的查詢或變更結果,需要一套完整的狀態機制來支撐。

例如:

  • 查詢送出前:loading = true
  • 查詢成功回應:loading = false, data = ...
  • 查詢失敗:loading = false, error = ...

但這一切狀態都得你親自操刀:

const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);

function fetchData() {
  setLoading(true);
  fetch('/graphql', ...)
    .then(res => res.json())
    .then(result => {
      setLoading(false);
      if (result.errors) {
        setError(result.errors);
      } else {
        setData(result.data);
      }
    })
    .catch(err => {
      setLoading(false);
      setError(err);
    });
}

問題是什麼?

  • 每次查詢都要重寫這段程式碼。
  • errordata 結構不同,每次處理都很麻煩。
  • 開發者常忘記在錯誤時設 loading = false,導致 UI 卡住。
  • 如果有多個查詢同時進行,狀態變得更加難以追蹤。

沒有快取系統:要效能,就得自己打造

GraphQL 的查詢雖然自由,但它沒有像 REST 那樣根據 URL 快取資料

舉例來說:

query {
  post(id: 1) {
    id
    title
  }
}

這筆查詢不會自動快取。

如果你之後再查一次 post(id: 1),還是會發送新請求。

為了避免重複請求、加快速度、減少用戶等待時間,開發者只能手動在 Redux、MobX 或自製的快取系統中儲存結果:

// 假設使用 Redux
dispatch({ type: 'CACHE_QUERY_RESULT', key: 'post-1', payload: result.data });

問題是什麼?

  • 每筆資料都要你定義 key,容易重複或衝突。
  • 修改一筆資料後,要手動更新所有與之關聯的快取資料。
  • 沒有內建的去重機制,畫面可能會不一致。
  • 快取邏輯與 UI 糾纏在一起,維護困難。

畫面資料更新困難,mutation 後要重發查詢或手動同步

假設你新增一筆留言:

mutation {
  addComment(postId: 1, content: "Nice article!") {
    id
    content
    createdAt
  }
}

這筆操作完成後,前端不會自動更新留言列表的畫面,你必須:

  1. 再次送出查詢 post(id: 1),重新拉留言列表。
  2. 或手動將新留言塞進狀態中(通常是 Redux):
dispatch({ type: 'APPEND_COMMENT', payload: newComment });

問題是什麼?

  • 每個畫面更新都要你自己設計「該更新哪些畫面」的邏輯。
  • mutation 與快取分離,不易整合。
  • 如果一頁多個元件共用資料,更新邏輯會變得更複雜,甚至會漏更新。

什麼是 Apollo Client?

Apollo Client 是一套功能完整的 GraphQL 客戶端函式庫,專為前端應用設計,讓開發者能夠輕鬆地與 GraphQL API 溝通、快取資料並更新使用者介面。

你可以把 Apollo Client 想像成是「前端與 GraphQL 後端之間的資料中介」,它幫你處理:

  • 查詢(Query)與變更(Mutation)操作
  • 快取與狀態同步
  • 錯誤處理與網路狀態追蹤
  • 與 UI 框架(如 React、Vue)整合
graph LR
    subgraph 前端應用
        UI[用戶界面組件]
        integrations["前端框架整合<br/>React (使用 @apollo/client)<br/>Vue (使用 vue-apollo)<br/>Angular (使用 apollo-angular)<br/>Svelte<br/>原生 JavaScript"]
        cache[Apollo Cache]
        AC[Apollo Client 核心]
    end

    subgraph 後端服務
        GS[GraphQL Server]
        DB[(資料庫)]
    end

    UI <--> integrations
    integrations <--> AC
    AC <--> cache
    AC <---> |GraphQL 查詢/變更| GS
    GS <--> DB

    style AC fill:#f5f5f5,stroke:#333,stroke-width:2px
    style cache fill:#f5f5f5,stroke:#333,stroke-width:1px
    style GS fill:#f5f5f5,stroke:#333,stroke-width:1px
    style UI fill:#f5f5f5,stroke:#333,stroke-width:1px

查詢與變更資料:免寫 fetch 的 GraphQL 封裝

Apollo Client 封裝了查詢(query)與變更(mutation)的邏輯。

只需簡單幾行程式碼就能與 GraphQL Server 溝通,不需再自行組裝 HTTP 請求。

使用 Apollo Client:

const { data, loading, error } = useQuery(GET_USERS);

🔴 傳統做法:

fetch('/graphql', {
  method: 'POST',
  body: JSON.stringify({ query: 'query { users { id name } }' })
});
功能傳統方式(純 fetch)Apollo Client
查詢語法提示無,拼錯欄位要等錯誤才知道有 VS Code 外掛與型別提示支援
變數傳遞自行處理 JSON 結構與變數格式用 variables 傳入即可
資料綁定到 UI手動撰寫狀態管理邏輯自動產出 loading, error, data

快取與畫面同步:In-Memory Cache 快速又一致

Apollo Client 內建快取(InMemoryCache)功能,會自動記住查詢結果,避免重複發送相同請求,並在資料更新時自動同步 UI。

使用 Apollo Client:

cache.modify({
  fields: {
    comments(existing = []) {
      return [...existing, newComment];
    }
  }
});

🔴 傳統做法:

dispatch({ type: 'APPEND_COMMENT', payload: newComment });
// Redux 中手動推入資料
功能傳統方式Apollo Client
快取儲存需自行在 Redux / localStorage 設計內建快取,自動依據 query 結果更新
多畫面資料同步自行追蹤資料來源、重複撰寫狀態同步邏輯快取即是單一資料來源,畫面會自動追蹤變化
mutation 後畫面更新需重發查詢或手動更新支援 update / cache.modify() 自動處理

錯誤與請求狀態處理:免寫樣板程式碼

每次發送請求都會有資料回傳、錯誤、載入中等狀態,Apollo 直接將這些封裝進 hook 回傳值中,不用每次都重新寫 try/catch 與 setLoading() 等邏輯。

使用 Apollo Client:

if (loading) return <p>載入中...</p>;
if (error) return <p>錯誤:{error.message}</p>;

🔴 傳統做法:

setLoading(true);
fetch(...)
  .then(res => res.json())
  .then(result => {
    setLoading(false);
    if (result.errors) {
      setError(result.errors);
    } else {
      setData(result.data);
    }
  });
功能傳統方式Apollo Client
錯誤處理自行寫 try/catch、錯誤判斷error 物件直接回傳
載入狀態追蹤自行控制 loading 狀態切換由 loading 屬性自動處理
請求狀態與 UI 結合需手動撰寫 if/else 或條件渲染hook 回傳值直接支援

框架整合:能用於各種現代前端框架

Apollo Client 核心與框架無關,能搭配任意 JavaScript 環境使用。它提供官方封裝套件與 hook,讓不同框架開發者都能享有一致體驗。

框架對應套件支援方式
React@apollo/client提供 useQuery, ApolloProvider
Vue@vue/apollo-composable提供 useQuery 等 Composition API
Angularapollo-angular提供服務與 decorator 支援
純 JSApolloClient + client.query()可直接操作 client 方法

Apollo Client 的核心組成:從建構到整合的基礎骨架

Apollo Client 雖然表面上看起來只是一個工具,但它其實是一個「前端資料層框架」,具備非常清楚的模組化設計。

這些模組互相分工合作,讓前端應用能穩定、快速且一致地操作 GraphQL 資料,以下是其三個最重要的核心元件:

ApolloClient:整個 GraphQL 資料系統的控制中樞

✅ 功能定位

ApolloClient 是 Apollo 架構的核心實例。

所有 GraphQL 請求的發送、快取管理、錯誤處理、網路設定、link middleware 等功能,都是透過這個實例統一控管的

你可以將它理解為「GraphQL 客戶端的總控室」,負責:

  • 定義要連去哪個 API
  • 怎麼處理每筆請求與回應
  • 如何儲存資料與自動同步 UI

✅ 基本初始化範例:

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',  // 設定 API 位址
  cache: new InMemoryCache(),              // 套用快取邏輯
});

🔍 背後的設計邏輯:

  • ApolloClient 實際上是一個狀態管理器 + HTTP 溝通者 + 快取引擎的組合
  • 你可以透過 client.query() 或 React hook 來發送請求
  • 每次的 query/mutation 都會走過 link middleware,支援認證、錯誤攔截、日誌記錄等功能

🆚 傳統做法對照:

功能傳統方式ApolloClient 的優勢
API 呼叫手動用 fetch / axios統一封裝在 client.query() 或 useQuery()
請求路徑與 header自行定義 URL、設定 headers使用 uri 與中介層 (link) 統一管理
多層功能鏈接必須層層撰寫 promise、錯誤處理等流程可使用 ApolloLink 串接多個功能模組

InMemoryCache:記憶體型快取系統,自動儲存與同步查詢資料

✅ 功能定位

InMemoryCache 是 Apollo 的快取系統,會自動記住查詢結果,並在需要時讀取或更新這些資料

快取不只是「記住資料」,還涉及到:

  • 根據查詢結構儲存欄位
  • 合併新資料與舊資料(如分頁、更新)
  • 根據物件識別碼自動去重、同步
  • 處理本地狀態與 client-only 欄位

✅ 如何初始化:

const cache = new InMemoryCache();

🔍 背後的核心運作:

  • 每個物件會依照其 __typenameid 作為唯一識別(可自訂)
  • 查詢結果會以「欄位為單位」儲存在一個類似 key-value 的記憶體結構中
  • 當 UI 請求相同資料時,會先讀快取(如果可用),避免重發請求
  • 當 mutation 修改某些欄位時,Apollo 會根據識別規則自動合併與更新快取

🔧 進階配置(typePolicies):

const cache = new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: ['uuid'],  // 改用自定義欄位作為主鍵
    },
    Query: {
      fields: {
        posts: {
          merge(existing, incoming) {
            return [...(existing || []), ...incoming];
          }
        }
      }
    }
  }
});

🆚 傳統快取方式對照:

功能傳統做法InMemoryCache 的優勢
快取建立與管理手動存資料進 Redux 或 localStorage自動根據查詢結構與 keyFields 儲存與同步
mutation 後的資料更新手動更新多個 UI 狀態與 store 資料自動依據物件識別碼更新相關元件資料
分頁與合併資料自行管理 offset / limit 數據堆疊可用 merge() 自訂快取資料如何組合
同一筆資料的多處使用狀態管理手動同步多元件間共用的資料所有元件讀取同一份快取資料,自動更新一致

ApolloProvider:將 Apollo 注入 React 應用的 Context 提供器

✅ 功能定位

ApolloProvider 是專屬於 React 的 Context 提供元件,用來將 ApolloClient 的實例注入到整個元件樹中。

當你用 useQuery()useMutation() 時,它們會自動去找最近一層的 ApolloProvider 提供的 client 實例,從而執行請求。

✅ 使用範例:

import { ApolloProvider } from '@apollo/client';

function App() {
  return (
    <ApolloProvider client={client}>
      <MainPage />
    </ApolloProvider>
  );
}

🔍 背後的運作方式:

  • 實作上等同於 React 的 Context.Provider
  • 所有子元件透過 React context 獲得 Apollo Client 實例
  • 支援多重 client(切換不同後端)或 nested provider(微前端架構)

🆚 傳統方式對照:

功能傳統 React 資料流ApolloProvider 的優勢
查詢上下文傳遞用 props 一層一層往下傳遞自動透過 React Context 傳遞 client
請求與 UI 綁定用 Redux middleware 綁 fetch搭配 useQuery、useMutation 自動處理
多頁面跨畫面資料同步使用 Global store 並寫邏輯同步多個元件直接共用快取狀態,自動同步

React 中的 Apollo 用法:查詢與變更

在 React 中,Apollo Client 提供了兩個核心 Hook 來操作 GraphQL:

  • useQuery:查詢資料,通常用於頁面初始化或需要即時更新的清單畫面。
  • useMutation:變更資料,例如新增、刪除、更新表單內容。

這些 Hook 封裝了請求、快取、錯誤處理、loading 狀態等邏輯,讓你能專注在 畫面與資料結構本身

useQuery:查詢資料,讓畫面與快取同步

✅ 使用範例

import { useQuery, gql } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
    }
  }
`;

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return <p>載入中...</p>;
  if (error) return <p>發生錯誤:{error.message}</p>;

  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

📌 背後運作流程(重要觀念)

  1. React 元件首次 render 時,useQuery 會向 ApolloClient 發送一個 Query 請求。
  2. Apollo 檢查快取:如果快取中有資料,就會立即回傳(同步)資料,同時也會再送一次網路請求確認更新。
  3. 如果快取中沒有資料,則等待伺服器回傳資料,並將結果寫入快取。
  4. 當快取更新,元件會自動重新 render 並顯示新資料。

⚙️ 常見選項與補充說明:

useQuery(GET_USERS, {
  variables: { id: 'abc' },
  skip: false,                  // 若為 true 則不執行查詢
  fetchPolicy: 'cache-first',   // 也可以是 'network-only', 'no-cache' 等
  onCompleted: data => { ... }, // 查詢成功後執行
  onError: error => { ... },    // 查詢失敗後執行
});

📌 何時該使用 refetch()

你可以透過 refetch() 方法在任意時機重新執行查詢:

const { data, refetch } = useQuery(GET_USERS);

<button onClick={() => refetch()}>重新取得資料</button>

這常用於:

  • 資料變更後(不用寫複雜快取更新)
  • 手動刷新頁面資料(例如拉下重新整理)

useMutation:變更資料,搭配表單、互動事件使用

✅ 基本範例(新增使用者)

import { useMutation, gql } from '@apollo/client';

const ADD_USER = gql`
  mutation AddUser($name: String!) {
    addUser(name: $name) {
      id
      name
    }
  }
`;

function AddUserForm() {
  const [name, setName] = useState('');
  const [addUser, { loading, error, data }] = useMutation(ADD_USER);

  const handleSubmit = async () => {
    try {
      await addUser({ variables: { name } });
      alert('新增成功!');
    } catch (err) {
      console.error('新增失敗', err);
    }
  };

  return (
    <>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button onClick={handleSubmit} disabled={loading}>新增</button>
      {loading && <p>送出中...</p>}
      {error && <p>錯誤:{error.message}</p>}
    </>
  );
}

📌 快取為什麼不會自動更新?

GraphQL mutation 只會影響資料庫,不會知道你快取了哪些查詢,所以 Apollo 不會主動更新快取(因為不知道要更新哪些 query)。

你需要:

  1. 手動更新快取(推薦)
  2. 或重新 refetch 某些查詢

✅ 手動更新快取(update()

const [addUser] = useMutation(ADD_USER, {
  update(cache, { data: { addUser } }) {
    cache.modify({
      fields: {
        users(existingUsers = []) {
          return [...existingUsers, addUser];
        }
      }
    });
  }
});

這樣寫代表:新增完後,自動在快取的 users 清單中插入新資料,無需重發 query。

✅ 自動重新查詢(refetchQueries

const [addUser] = useMutation(ADD_USER, {
  refetchQueries: ['GetUsers'],
});

這代表新增完後,自動重新執行 GetUsers 查詢,讓資料同步。

優點:簡單。
缺點:會多發一次請求,較耗資源。

useQuery vs useMutation 對照

功能useQueryuseMutation
目的從伺服器取得資料向伺服器傳送變更指令(新增、更新、刪除)
自動執行是,component mount 時執行否,需手動呼叫
是否影響快取是,會寫入快取否,需自行更新快取或 refetch
快取更新方式自動update() 或 refetchQueries
常見用途清單顯示、個人資料載入表單送出、新增留言、刪除貼文等操作

開發實務建議與最佳做法

情境建議做法
mutation 後需立即顯示新資料使用 update() 寫入快取,或 refetchQueries
多個 mutation 接連發生使用 async/await 搭配 try/catch 控制錯誤與 loading
需要查詢但初始時機不確定改用 useLazyQuery 手動控制查詢時機
mutation 有不同輸入欄位組合配合表單工具(如 Formik、React Hook Form)整理狀態
快取與後端資料不一致時用 fetchPolicy: 'network-only' 強制重新拉資料
想監控請求狀態與分析錯誤可加上 onCompleted, onError callback 追蹤邏輯

實務建議:初學者該怎麼學 Apollo Client?

Apollo Client 功能強大,但對初學者來說也可能讓人一開始感到有些複雜。

它涵蓋了快取、網路請求、狀態管理、UI 同步、型別自動化等議題,因此採取正確的學習方式將大幅減少撞牆期。

以下是循序漸進的實務學習建議:

從基礎學會 GraphQL 查詢語法與概念

Apollo 是建構在 GraphQL 協定之上的,因此必須先熟悉以下核心語法:

類型說明範例
Query查詢資料(等同於 REST 的 GET)query { users { id name } }
Mutation修改資料(等同於 POST/PUT/DELETE)mutation { addUser(name: "Amy") { id } }
變數傳遞將查詢參數抽離,增加彈性與可維護性query ($id: ID!) { user(id: $id) { name } }

🔍 建議工具/資源:

從小型實作專案練習 Apollo Client 的查詢與變更流程

初期不要急著寫複雜的應用,而應該從具有明確資料流的小專案入手,以下是推薦的三個實戰方向:

專案範例可練習的重點
✅ Todo ListuseQuery 查詢 todos,useMutation 新增/刪除
✅ 使用者清單快取合併、update() 更新列表、refetch 練習
✅ 部落格文章頁面根據 ID 查詢單篇文章、帶參數查詢、留言送出後同步畫面

🔍 實作時建議:

  • 一次只使用一種查詢或變更邏輯,釐清每個 Hook 的行為。
  • 利用 Chrome DevTools 看網路請求是否發出預期的 query。
  • 開啟 Apollo DevTools 看快取結構與資料流。

學會快取原理與更新策略,是進階關鍵

很多初學者以為「用 Apollo 就不用管快取了」,其實正好相反:Apollo Client 最大的價值來自它的快取系統(InMemoryCache),而這也是最容易出錯的地方

以下是你應該理解的快取知識:

快取議題初學者常見問題建議學會的知識點
mutation 不更新畫面為何按了新增,畫面卻沒更新?update() 的用途與 cache.modify() 寫法
需要重新查詢畫面資料新增成功但想直接重查整個列表使用 refetchQueries
想控制哪些欄位快取同樣的 query,但部分欄位不想快取使用 fetchPolicy 如 no-cache
複雜欄位資料錯位多頁分頁載入資料合併後順序錯誤使用 merge() 自訂欄位合併邏輯

🔍 推薦練習方式:

  • Apollo DevTools 查看快取欄位結構
  • 使用 console.log(cache.extract()) 檢查快取內容
  • 練習手動編寫 update()cache.modify() 更新快取資料

搭配 Codegen 自動產生 TypeScript 型別,減少錯誤與重工

GraphQL 查詢結果是動態的,若搭配 TypeScript,手動寫型別不僅繁瑣,也很容易出錯。這時可以導入 GraphQL Code Generator 工具,幫你自動產生查詢對應的型別與 React Hooks!

📦 它可以幫你做什麼?

  • 根據 .graphql 檔案,自動產生:
  • TypeScript 型別
  • useQueryuseMutation 等 Hook
  • 查詢結果的類型推導與 props 類型支援

📋 常見配置方式:

schema: https://your-graphql-server.com/graphql
documents: './src/**/*.graphql'
generates:
  ./src/generated/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'

🔍 整合建議:

  • 所有查詢寫成 .graphql 檔案統一管理
  • 配合 IDE 提示與 TS 型別補全,避免欄位拼錯
  • 在 PR 或 CI 流程中自動執行 codegen,確保型別一致性

將 Apollo 納入實戰工作流程,才是真正會用

學習 Apollo Client 並不只是「資料能拿回來就好」,而是要把它真正納入元件設計、資料流程、頁面快取策略當中:

工作情境Apollo Client 的應用方式
一頁多筆資料來源多組 useQuery 並存,分開處理 loading 狀態
查詢結果要在多畫面共用利用快取共用資料,並避免重複 query
mutation 成功後跨畫面同步使用 cache.modify 或 refetchQueries 保持一致
動態表單送出 / 編輯頁面配合 React Hook Form 管理欄位狀態,mutation 後再觸發畫面更新
頁面切換後仍保留上次資料狀態保留預設快取行為,讓切換回頁面不必再次載入

總結:初學者學 Apollo Client 的心法

建議方向原因或收穫
先從 GraphQL 語法入門Apollo 的本質是 GraphQL 工具,語法熟悉才能靈活使用
一次練一種功能查詢與變更的邏輯大不相同,拆開學習會降低混淆
用 DevTools 看快取與請求把 Apollo 當成資料管理工具來 debug,理解資料怎麼進出會更有感
別急著上大型應用,從小做起小範圍驗證 useQuery / useMutation / 快取邏輯能快速培養信心與理解
善用工具:Codegen 與 Playground提升開發體驗、型別安全、除錯效率,是職場工作者幾乎必備的工具鏈整合技能

Similar Posts