用 useMutation 寫入資料:新增 / 更新都搞定

更新日期: 2025 年 6 月 10 日

GraphQL 不只有查詢資料(Query),也可以新增、更新、刪除資料(Mutation)

在前端使用 Apollo Client 時,useMutation 就是我們操作伺服器資料的主力工具

舉例來說,你可能會需要:

  • 新增一筆留言或使用者
  • 更新某個表單送出的資料
  • 按下按鈕刪除某個項目

這些動作,就要靠 useMutation 來完成。

什麼是 Mutation?與 Query 的差別在哪?

在 GraphQL 中,所有資料操作都分為兩大類型:

  1. Query(查詢):用來「讀取」資料。
  2. Mutation(變更):用來「寫入或改變」資料。

可以這麼理解:

Query 是在問伺服器:「你有什麼資料可以給我看?」

Mutation 則是在請求伺服器:「幫我新增 / 修改 / 刪除某筆資料。」

以下是這兩者的差異整理:

功能Query(查詢)Mutation(寫入)
用途讀取資料寫入資料(新增 / 更新 / 刪除)
GraphQL 關鍵字querymutation
回傳內容查詢到的資料結果操作後的結果或資料
操作特性不改變伺服器資料,只是查詢改變資料庫內容,具有副作用
範例情境查看使用者清單、產品明細註冊新帳號、編輯留言、刪除文章

為什麼需要 Mutation?

許多網頁或系統除了顯示資料,也會讓使用者互動式地「送出表單」、「按按鈕」、「建立新項目」。

這些動作背後幾乎都對應一個資料改變的請求。

這些情境都屬於「具有副作用的操作」:

  • 新增資料:註冊帳號、留言、建立訂單
  • 修改資料:編輯個人資訊、更新購物車
  • 刪除資料:移除帳號、取消訂單

這時就要使用 GraphQL 的 mutation,才能將這些變更傳送給後端伺服器處理。

思維轉換:Mutation 就像是一段「後端函式」,你在前端呼叫並取得回傳值

對於有 REST API 開發經驗的工程師來說,可以把 GraphQL 的 Mutation 想像成一段具名的、可參數化的後端函式

你在前端傳入參數,它會在後端執行一整段資料處理邏輯,最後回傳你想要的結果。

舉個例子,假設你在系統中要「建立使用者」,後端可能會做以下幾件事:

  • 驗證使用者名稱與 email 格式是否正確
  • 檢查該 email 是否已被註冊
  • 將資料寫入資料庫
  • 回傳新建立的使用者 ID 與資料(這點很關鍵!我們等下會深入解釋)

這在 REST API 中,可能會對應一個 POST /users 的請求。

而在 GraphQL 中,則會寫成如下的 mutation:

mutation {
  createUser(name: "小明", email: "[email protected]") {
    id
    name
    email
  }
}

在後端,這段 mutation 通常會對應一個 Resolver 函式:

// 在後端的 createUser Resolver
function createUser(parent, args, context) {
  const { name, email } = args;
  // 資料驗證、資料庫操作邏輯
  const user = db.users.create({ name, email });
  return user; // 回傳給前端
}

這就像是你提供參數、後端執行邏輯,最後回傳結果給你。

這是 GraphQL 最強大的設計之一:前後端互動就像在調用函式,具名、明確、可控制結果格式。

更強大的地方:你可以選擇「回傳哪些欄位」

不同於 REST API 固定回傳所有欄位(例如你可能會拿到完整的使用者物件),GraphQL 最大的特色之一就是回傳欄位可以自訂,只拿你需要的欄位

舉個例子,我們發送以下兩種 mutation 請求:

# 請求回傳 id 和 email
mutation {
  createUser(name: "小明", email: "[email protected]") {
    id
    email
  }
}
# 請求只回傳 name
mutation {
  createUser(name: "小明", email: "[email protected]") {
    name
  }
}

上面兩個請求都是呼叫 createUser,但回傳資料的欄位不同。這代表:

你可以根據畫面需求,只請求要用到的欄位
不用傳回不必要的資料,節省流量、提升效能

❓那我要怎麼知道我能選哪些欄位?

GraphQL 的欄位其實來自於後端定義的 Schema

只要你知道一個操作(像是 createUser)的輸入參數和回傳結構,就可以決定要不要選取其中的某些欄位。

你可以用以下幾種方式查看可選欄位:

✅ 使用開發工具(推薦)

  • Apollo Studio Explorer:視覺化工具,會顯示每個查詢或 mutation 可用的欄位。
  • GraphQL Playground / GraphiQL:類似 Postman 的工具,打開後自動補全可用欄位。
  • VS Code 插件(如 GraphQL extension):在寫 gql 語法時會自動跳出欄位選單。

這些工具會根據後端的 Schema 顯示你可選的欄位,像這樣:

# 在 GraphQL Playground 打 createUser 時會自動補出:
{
  id
  name
  email
  createdAt
}

✅ 直接查看 Schema 文件

如果後端有開放 .graphql.json 格式的 Schema 文件,你也可以用 CLI 工具下載:

# 使用 graphql-cli 匯出 Schema
graphql get-schema --endpoint=http://localhost:4000/graphql

# 使用 Apollo CLI 匯出 Schema
npx apollo schema:download --endpoint=http://localhost:4000/graphql schema.json

從 Schema 中可以看到每個 mutation 的回傳型別,例如:

type Mutation {
  createUser(name: String!, email: String!): User!
}

type User {
  id: ID!
  name: String!
  email: String!
  createdAt: String!
}

這樣你就知道 createUser 回傳的是一個 User,而 User 有哪些欄位可以選擇。

為什麼不能只回傳 success: true

有些人可能會問:「我只想知道操作成功與否,不就回傳 success: true 就好了嗎?」

理論上可以,但實務上會有很多問題與不便,以下三個重點說明:

✅ 1. 前端通常需要新資料進行後續操作

  • ⛳ 你可能需要跳轉頁面,像是:
  navigate(`/users/${data.createUser.id}`)
  • 📋 或是你想即時更新畫面,把這筆新資料顯示在列表中:
  setUsers(prev => [data.createUser, ...prev]);

若後端只回傳「成功」,你得再發一個 Query 去查剛剛那筆資料,等於多一次請求 + 效能浪費 + 資料不同步風險

🚀 2. 避免多一次查詢,節省效能與時間

GraphQL 的設計理念之一,就是前後端協調好一次傳完需要的資料,減少重複呼叫

如果你發出一個 mutation,卻還要發 query 把資料撈回來,那就失去了 GraphQL 一次請求、資料裁切的優勢。

🔁 3. 讓 Apollo 自動更新快取,維持狀態一致

Apollo Client 有強大的快取系統,當你回傳資料時,它可以自動根據 id 等欄位更新快取,讓其他使用 useQuery 的畫面也即時同步。

但如果你只回傳 success: true,Apollo 無法幫你更新快取,你就得手動處理同步,開發成本更高、錯誤機率更大

useMutation 的基本用法

在 Apollo Client 中,useMutation 是專門用來發送 GraphQL 的 Mutation 請求(也就是新增、修改、刪除資料的操作)。

這一段會帶你一步步學會:

  1. 定義 mutation 語法
  2. 呼叫 useMutation 並了解它的回傳內容
  3. 實際執行 mutation 並處理資料與狀態

第一步:定義 gql 語法(GraphQL Mutation 語法)

在使用 useMutation 之前,我們需要先定義一段 GraphQL 的 Mutation 語法。

這段語法就像是「你要做什麼操作、需要哪些參數、希望回傳什麼欄位」的說明書。

Apollo Client 提供了 gql 這個標記模板函式(tagged template function),用來撰寫 GraphQL 查詢語法。

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

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

🧠 逐行解析說明:

語法片段意義
mutation CreateUser(...)宣告這是一個 Mutation 操作。CreateUser 是這個操作的名稱,可以自訂(也可以省略)。
$name: String!, $email: String!這是變數定義區。用 $ 宣告變數名稱,String! 表示這個變數是字串,且不能為空(非 null)。
createUser(...)這是後端提供的 Mutation 函式名稱(Schema 定義的),你要呼叫它來新增資料。
name: $name, email: $email把變數帶入這個函式作為參數,類似於 JS 函數的傳參。
{ id name email }表示你希望伺服器回傳的欄位。你只會拿到這裡選的資料,其他欄位(例如密碼)不會自動出現。

📌 記得:這段語法只是一個「定義」,它不會真的執行,必須搭配 useMutation 來發送請求。

第二步:使用 useMutation,取得「觸發函式」

const [createUser, { data, loading, error }] = useMutation(CREATE_USER);

🧠 什麼是「觸發函式」?

這裡的 createUser 就是你可以呼叫的 mutation function,又稱「觸發函式」。

這個函式來自 Apollo Client,它根據你提供的 gql 語法自動產生,內建好以下功能:

  • 幫你包裝好 HTTP 請求格式
  • 自動處理變數插入
  • 負責傳送資料給 GraphQL Server
  • 收到回應後會更新 data / error / loading 狀態

簡單來說:

graph LR
    A["定義 GQL"] --> B["Apollo 產生 createUser"]
    B --> C["呼叫 createUser()"]
    C --> D["發送 mutation 請求"]
    
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#fff3e0
    style D fill:#e8f5e8

✨ 呼叫方式:

createUser({
  variables: {
    name: '小明',
    email: 'xiaoming@example.com'
  }
});

你只需要在事件中呼叫這個函式並傳入變數,它就會自動幫你處理整個請求流程。

初學者常見疑問解答

【為什麼 GraphQL 不能用 ${} 插入變數?】
const CREATE_USER = gql`
  mutation {
    createUser(name: ${name}, email: ${email}) { ... }
  }
`;

這樣寫會報錯,因為:

GraphQL 是一種獨立語言,不能像模板字串那樣用 ${} 插入 JavaScript 變數

正確做法是使用 GraphQL 的變數機制,再透過 variables 傳入:

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
    }
  }
`;

createUser({
  variables: { name, email }
});

✅ 這樣的好處包括:

  • 避免 XSS 風險與語法錯誤
  • 可支援快取與查詢重用
  • 支援型別檢查(有 IDE 提示)
❓【useMutation() 回傳什麼?命名方式是什麼?】

Apollo 的 useMutation(...) 回傳的其實是一個陣列,你可以透過 陣列解構賦值 自由命名裡面的元素。

const [mutateFunction, resultObject] = useMutation(MY_MUTATION);

這個陣列結構是固定的:

陣列位置內容用途說明
[0]函式這是你觸發 mutation 的函式,常命名為 createXxx
[1]狀態物件包含 mutation 的執行狀態與結果,例如:data、loading、error

📌 命名原則建議:

元素命名慣例說明
GraphQL 文件名全大寫 + 底線(CREATE_USER)表示它是常數、不可變的 GQL 物件
觸發函式(第一個值)小駝峰(createUser)表示這是一個可以呼叫的函式
結果物件(第二個值)可為 result、mutationResult 等包含 loading、error、data 狀態

✅ 你可以這樣命名:

const [createUser, { data, loading, error }] = useMutation(CREATE_USER);

也可以這樣:

const [sendUserData, result] = useMutation(CREATE_USER);

👉 重點是:命名自由,語意清楚即可。Apollo 並不會強制你取特定名稱,也不會自動幫你產生名稱。

第三步:發送 Mutation 請求(通常搭配事件觸發)

當你定義好 gql 語法並用 useMutation 建立好了觸發函式(如 createUser),

你就可以在任何時機呼叫這個函式,主動發送 mutation 請求

這通常會搭配以下幾種情境:

  • 使用者點擊按鈕(onClick
  • 表單送出(onSubmit
  • 狀態變更時(如某個條件成立時自動發送)

✅ 實際使用範例(用 onClick 執行)

const handleCreate = () => {
  createUser({
    variables: {
      name: '小明',
      email: '[email protected]'
    }
  });
};

return <button onClick={handleCreate}>建立使用者</button>;

這段程式碼的意思是:

  1. 使用者點擊按鈕時會執行 handleCreate
  2. handleCreate 裡呼叫 createUser()(你從 useMutation() 拿到的觸發函式)
  3. 傳入的 variables 物件會被自動注入進 gql 中的 $name$email

📌 variables 的命名要對得上

GraphQL 的變數使用方式有個重點:你在 gql 語法中定義了哪些 $xxx 變數,就必須在 variables 裡提供對應名稱的資料。

以這段 gql 為例:

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
    }
  }
`;

這裡你定義了兩個變數:

  • $name → 必須提供 name
  • $email → 必須提供 email

所以正確的 variables 寫法會是:

variables: {
  name: '小明',
  email: '[email protected]'
}

🚫 錯誤寫法舉例(初學者常見)

variables: {
  username: '小明', // ❌ 沒有對應到 `$name`
  email: '[email protected]'
}

這會導致錯誤訊息:

Variable "$name" of required type "String!" was not provided.

💡 延伸實作:搭配表單輸入

通常這個變數不是寫死,而是由使用者在表單中輸入,我們會搭配 useState() 來取得輸入內容:

const [name, setName] = useState('');
const [email, setEmail] = useState('');

const handleSubmit = (e) => {
  e.preventDefault(); // 阻止表單預設行為
  createUser({
    variables: { name, email }
  });
};

配合表單 UI:

<form onSubmit={handleSubmit}>
  <input value={name} onChange={(e) => setName(e.target.value)} placeholder="姓名" />
  <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="信箱" />
  <button type="submit">送出</button>
</form>

這樣你就完成了一個互動式的 mutation 請求流程,從輸入 ➜ 點擊 ➜ 發送 ➜ 拿到資料。

補充:mutation 執行結果去哪了?

當你呼叫 createUser 後,Apollo Client 會自動更新以下狀態:

狀態變數說明
loading發送中為 true,可用來顯示 loading 動畫或禁止按鈕
error若發生錯誤(如後端驗證失敗),這裡會存錯誤資訊
data如果成功,這裡會存你要求的回傳欄位(例如 data.createUser.id)

你可以加上 UI 呈現:

{loading && <p>建立中...</p>}
{error && <p style={{ color: 'red' }}>{error.message}</p>}
{data && <p>註冊成功!歡迎 {data.createUser.name}</p>}

🔄 實務上補充:成功後常見動作

當 mutation 成功後,開發者通常會進一步:

  • 清空表單欄位:setName(''); setEmail('');
  • 顯示提示訊息或導頁:alert('註冊成功')navigate('/dashboard')
  • 更新畫面資料或觸發 refetch

與 Query 並用的情境:Mutation 成功 ≠ 畫面自動更新

GraphQL 的 Mutation 主要用來執行「改變資料狀態」的操作,像是新增使用者、更新留言內容、刪除一筆訂單等。

相對地,Query 則是用來「讀取伺服器資料」的操作,例如讀取所有留言、顯示目前登入的使用者資料。

這兩者在語法與功能上是分開的,也在 邏輯意圖上刻意分離,但初學者常會有一個誤解:

❓「我不是剛剛用 useMutation 新增了一筆資料嗎?畫面為什麼沒有更新?」

這就是我們今天要解釋的重點:

🧠 Mutation 雖然能回傳資料,但它不會自動幫你更新使用 Query 拿回來的資料畫面

這是 GraphQL 的一個設計特色,也是與 REST API 思維不同的地方:

REST API 做法GraphQL 做法
常常由後端控制:改完資料就回傳新的一整包 JSON由前端決定要回傳哪些欄位,但回傳資料只是一份「結果」
有時候資料變了,畫面自動重載GraphQL 回傳結果後,快取和其他畫面狀態不會自動變動

也就是說,在 GraphQL 中,Mutation 負責「改資料」,但 Query 的畫面更新要你自己決定要不要改。

為什麼不設計成「自動幫我更新」?

GraphQL/Apollo 的設計哲學是:

「資料主權在你手上,畫面要不要更新、怎麼更新,由你決定。」

這樣做有三個原因:

  1. 彈性更高:不同頁面可能要更新不同部分的資料,不能統一套用
  2. 效能更好:如果你只需要那一筆資料,沒必要重新查整份列表
  3. 狀態更可控:資料更新邏輯集中於前端,避免副作用擴散

這雖然給你更多控制權,但也意味著:你要自己決定何時更新畫面。

❗常見錯誤示範

const [createComment] = useMutation(CREATE_COMMENT);
const { data } = useQuery(GET_COMMENTS);

const handleSubmit = async () => {
  await createComment({ variables: { text: '留言內容' } });
  // 以為畫面會自動更新,但其實完全不會動!
};

這段程式碼會成功新增資料到後端,但畫面上的留言列表 data.comments 並不會更新。

因為 GET_COMMENTS 的 Query 還停留在原本快取的狀態,GraphQL 不會自動「重新撈一次」或「自動合併回傳資料」。

  • Mutation 回傳的是一筆資料操作的結果
  • Query 負責畫面上的資料來源
  • 兩者是邏輯上獨立的 → 所以你得告訴 Query:「我要更新了」

接下來我們就來看看,當你使用 Mutation 操作資料時,有哪些做法可以讓 Query 的畫面也跟著更新。

情境一:頁面初始化 → 用 Query 撈出資料列表

const { data } = useQuery(GET_COMMENTS);

這會撈出一整包留言,例如畫面上顯示的十筆留言。

情境二:使用者送出留言 → 用 Mutation 寫入資料

createComment({ variables: { text: newComment } });

這筆留言確實新增成功了,但畫面上那份透過 GET_COMMENTS 撈出的列表並不會自動加上去

這時你有兩種方式可以讓畫面資料跟著更新:

🔁 方法一:使用 refetch() 重新查詢最新資料

這是最簡單、最保險的做法,直接重撈一次整份資料:

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

const [createComment] = useMutation(CREATE_COMMENT, {
  onCompleted: () => {
    refetch(); // 再撈一次留言列表
  }
});

這個方式的好處是簡單直接,尤其對初學者或資料結構複雜的情境來說,是最穩定的做法。

但缺點也明顯:

  • 每次送出都會打兩次 API(Mutation + refetch)
  • 如果資料量大,會造成效能負擔與畫面延遲

🔧 方法二:手動更新 Apollo 快取,立即更新畫面

如果你不想再打一個 Query,可以選擇更進階的方式 —— 手動更新快取

const [createComment] = useMutation(CREATE_COMMENT, {
  update(cache, { data }) {
    const newComment = data.createComment;
    const existing = cache.readQuery({ query: GET_COMMENTS });

    cache.writeQuery({
      query: GET_COMMENTS,
      data: {
        comments: [newComment, ...existing.comments],
      }
    });
  }
});

這樣做可以達到以下目的:

  • 使用者送出留言後,畫面立即看到更新
  • 不需重新請求後端
  • 快取與畫面資料同步,效能最佳化

但這需要對 Apollo Cache 操作有一定理解,適合進階開發者使用。

🧰 方法三:用 useState() 自己管理資料列表

如果你不是用 Apollo 快取來管理列表,而是單純用 React state,也可以直接在前端新增那筆資料:

const [comments, setComments] = useState([]);

createComment({ variables: { text } }).then(res => {
  setComments(prev => [res.data.createComment, ...prev]);
});

這是最簡易的方式,適合畫面不複雜、資料不需共享的元件內部使用。

結語:Mutation 是 GraphQL 最重要的互動機制

useMutation 就像是你在前端與資料庫對話的方式,無論是送出表單、更新個資、刪除留言,它都能幫你快速完成資料操作。

學會 useMutation,你就學會了前端 GraphQL 的一大核心技能。

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *