用 useMutation 寫入資料:新增 / 更新都搞定
更新日期: 2025 年 6 月 10 日
GraphQL 不只有查詢資料(Query),也可以新增、更新、刪除資料(Mutation)。
在前端使用 Apollo Client 時,useMutation
就是我們操作伺服器資料的主力工具。
舉例來說,你可能會需要:
- 新增一筆留言或使用者
- 更新某個表單送出的資料
- 按下按鈕刪除某個項目
這些動作,就要靠 useMutation
來完成。
什麼是 Mutation?與 Query 的差別在哪?
在 GraphQL 中,所有資料操作都分為兩大類型:
- Query(查詢):用來「讀取」資料。
- Mutation(變更):用來「寫入或改變」資料。
可以這麼理解:
Query 是在問伺服器:「你有什麼資料可以給我看?」
Mutation 則是在請求伺服器:「幫我新增 / 修改 / 刪除某筆資料。」
以下是這兩者的差異整理:
功能 | Query(查詢) | Mutation(寫入) |
---|---|---|
用途 | 讀取資料 | 寫入資料(新增 / 更新 / 刪除) |
GraphQL 關鍵字 | query | mutation |
回傳內容 | 查詢到的資料結果 | 操作後的結果或資料 |
操作特性 | 不改變伺服器資料,只是查詢 | 改變資料庫內容,具有副作用 |
範例情境 | 查看使用者清單、產品明細 | 註冊新帳號、編輯留言、刪除文章 |
為什麼需要 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 請求(也就是新增、修改、刪除資料的操作)。
這一段會帶你一步步學會:
- 定義 mutation 語法
- 呼叫
useMutation
並了解它的回傳內容 - 實際執行 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>;
這段程式碼的意思是:
- 使用者點擊按鈕時會執行
handleCreate
handleCreate
裡呼叫createUser()
(你從useMutation()
拿到的觸發函式)- 傳入的
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 的設計哲學是:
「資料主權在你手上,畫面要不要更新、怎麼更新,由你決定。」
這樣做有三個原因:
- ✅ 彈性更高:不同頁面可能要更新不同部分的資料,不能統一套用
- ✅ 效能更好:如果你只需要那一筆資料,沒必要重新查整份列表
- ✅ 狀態更可控:資料更新邏輯集中於前端,避免副作用擴散
這雖然給你更多控制權,但也意味著:你要自己決定何時更新畫面。
❗常見錯誤示範
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 的一大核心技能。