# TanStack Query (React Query) vs. Apollo Client,兜幾?
## ✨ 核心理念
:::success
**TanStack Query (舊名: React Query)** [Doc](https://tanstack.com/query/v5)
> Powerful asynchronous state management for TS/JS, React, Solid, Vue, Svelte and Angular
:::
:::success
**Apollo Client** [Doc](https://www.apollographql.com/docs/react)
> Apollo Client is a comprehensive state management library for JavaScript. It enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI.
:::
#### 共同點
- 都是 state management,主要是為了管理 fetch data
#### 差異處
- Apollo Client 主要為 GraphQL 設計
- Apollo Client 除了「管理 API 的資料」外,也支援 *local* 的資料 ([Local state management](https://www.apollographql.com/docs/react/v2/data/local-state)),代表可以像使用其他狀態管理一樣自行塞資料進去,實際有多少人這樣做不知道...
- TanStack Query 官方提供多框架支援,包括 React、Vue、Solid、Svelte 和 Angular;Apollo Client 官方專注於 React,其他框架如 Angular、Vue 和 Svelte 需要使用社群開發的整合套件 ([View integrations](https://www.apollographql.com/docs/react/integrations/integrations))
### 📝 使用調查
#### NPM 下載量比較 ([來源](https://npmcharts.com/compare/@tanstack/react-query,react-query,@apollo/client,@apollo/react-hooks?interval=7&log=false))
* TanStack Query (react) = `@tanstack_react-query` (v4, v5) + `react-query` (v3)
* Apollo Client = `@apollo_client` (v2) + `@apollo_react-hooks` (v3)

#### 2023 滿意度 ([來源](https://2023.stateofreact.com/en-US/libraries/)) *新的還沒出來*

💡 更想知道:在使用 graphql 的前提下,前端有多少比例選擇用 react-query?
相關討論:[Apollo Client vs React-Query. Help me choose. Do I need normalized cache?](https://www.reddit.com/r/reactjs/comments/oqx5me/apollo_client_vs_reactquery_help_me_choose_do_i/)
> 朋友公司之前使用 graphql + typescript + react-query + codegen
codegen 參考文章:[在 React 前端自動產生 GraphQL operation 的 type 達到更好的開發體驗](https://jigsawye.com/2021/04/28/graphql-codegen)
:::warning
🏁 以下簡稱 `react-query` 和 `apollo`
:::
## 主要設計
### 🗺️ 查詢流程
- **react-query**
```
QueryClient -> QueryCache (Queries) <-> QueryObserver
```

<small>來源:https://tkdodo.eu/blog/inside-react-query</small>


<small>來源:https://mini-ghost.dev/posts/tanstack-query-source-code-1</small>
- **apollo**
```
Apollo Client <-> InMemoryCache (normalized cache)
```

<small>來源:https://www.apollographql.com/docs/react/caching/overview</small>

<small>來源:https://www.slideshare.net/slideshow/getting-started-with-apollo-client-and-graphql/89238484</small>
### ⚙️ Default Settings
- **react-query**
| **設定** | **預設值** | **描述** |
|-----------------------|-----------------------------|-----------------------------------------------------------------------------------------------------|
| **`gcTime`(原本名稱`cacheTime`)** | `1000 * 60 * 5` (5 分鐘) | 「**非活躍 (inactive)**」查詢的快取資料會在 5 分鐘後被清理(Garbage Collection) |
| **`staleTime`** | `0` | 收到資料立刻就轉為 **stale(過期)**,每次重新載入都會觸發 refetch |
| **`retry`** | `3` | 查詢發生錯誤時,最多會自動重試 3 次 |
| **`refetchOnWindowFocus`** | `true` | 當視窗重新獲得焦點時(ex: 從別的分頁切回來),自動 refetch |
| **`refetchOnReconnect`** | `true` | 當網路重新連接時,自動 refetch |
- **apollo**
| **設定** | **預設值** | **描述** |
|-----------------------|-----------------------------|-----------------------------------------------------------------------------------------------------|
| **`fetchPolicy`** | `cache-first` | 使用快取資料,若快取中沒有資料才會發送請求到 server |
| **`resultCaching`** | `true` | 啟用結果快取,重複的查詢不會觸發網路請求,直接從快取中讀取結果 |
| **`errorPolicy`** | `none` | 在遇到錯誤時 return `error.graphQLErrors` ,data 為 `null` |
💡 補充討論:[I just wanted to know if there is a specific reason not to have errorPolicy: "all" as default.](https://github.com/apollographql/apollo-client/issues/11682)
### 🤖 基本語法範例
- **react-query**
```javascript=
function fetchTodos() {
return fetch(`https://jsonplaceholder.typicode.com/todos`).then(
response => response.json()
);
}
function postTodo(newTodo) {
return fetch(`https://jsonplaceholder.typicode.com/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
}).then(response => response.json());
}
// Queries
const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
<button onClick={() => mutation.mutate({ title: 'New Todo', completed: false })}>Add Todo</button>
```
- **apollo**
```javascript=
// GraphQL Queries
const GET_TODOS = gql`
query GetTodos {
todos {
id
title
completed
}
}
`;
const ADD_TODO = gql`
mutation AddTodo($title: String!, $completed: Boolean!) {
addTodo(input: { title: $title, completed: $completed }) {
id
title
completed
}
}
`;
// Queries
const { loading, error, data } = useQuery(GET_TODOS);
// Mutations
const [addTodo] = useMutation(ADD_TODO, {
// Refetch the GET_TODOS query after success
refetchQueries: [{ query: GET_TODOS }],
});
```
### 💾 Cache 快取資料管理
#### 🔍 識別方式
- **react-query** - 使用 `queryKey` [Doc](https://tanstack.com/query/v4/docs/framework/react/guides/query-keys)
```javascript=
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos
});
useQuery({
queryKey: ['todos', todoId],
queryFn: () => fetchTodoById(todoId),
});
useQuery({
queryKey: ['todos', { status, page }],
queryFn: fetchTodos
});
```
- **apollo** - 預設使用 GraphQL reponse 回的 `__typename` 和 `id`
- 資料標準化 Normalized Data:Apollo 會根據 `__typename` 和 `id`,將返回的資料儲存為單獨的記錄,並建立與查詢的關聯
- 可以自定義識別符,使用 typePolicies 配置 keyFields
```javascript=
// 預設情況:基於 `__typename` 和 `id`
const GET_TODOS = gql`
query GetTodos {
todos {
id
title
completed
}
}
`;
// 自定義 ID 識別
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['email'], // 使用 email 作為唯一標識符
},
},
}),
});
```
```json=
// Normalized data
{
"ROOT_QUERY": {
"todos": ["Todo:1", "Todo:2"]
},
"Todo:1": {
"id": 1,
"title": "Learn GraphQL",
"completed": true
},
"Todo:2": {
"id": 2,
"title": "Write Code",
"completed": false
}
}
```
💡 補充討論: [I need to remove __typename for each mutation. I do not want to complain. I am just asking if it is feature or bug.](https://github.com/apollographql/apollo-client/issues/1913)
#### 🚪 訪問快取資料
- **react-query** [Doc](https://tanstack.com/query/v5/docs/reference/QueryClient)
```javascript=
const queryClient = useQueryClient();
// 讀取快取
const todos = queryClient.getQueryData(['todos']);
// 寫入快取
queryClient.setQueryData(['todos'], (oldData) => [
...oldData,
{ id: 4, title: 'New Todo', completed: false },
]);
// 清除快取
queryClient.removeQueries(['todos']);
```
- **apollo** [Doc](https://www.apollographql.com/docs/react/caching/cache-interaction)
- `readQuery`:讀取快取中某個查詢的結果
- `writeQuery`:向快取寫入查詢的結果
- `readFragment`:讀取快取中某個片段 (fragment) 的資料
- `writeFragment`:向快取寫入片段 (fragment) 的資料
```javascript=
// 讀取快取
const todos = client.readQuery({
query: GET_TODOS,
});
// 寫入快取
client.writeQuery({
query: GET_TODOS,
data: {
todos: [
{ id: 1, title: 'Learn GraphQL', completed: true },
{ id: 2, title: 'Build an App', completed: false },
],
},
});
// 讀取片段
const todo = client.readFragment({
id: 'Todo:1', // 必須包含 __typename 和 ID
fragment: gql`
fragment TodoFragment on Todo {
title
completed
}
`,
});
// 修改片段
client.writeFragment({
id: 'Todo:1',
fragment: gql`
fragment TodoFragment on Todo {
title
completed
}
`,
data: {
completed: true,
},
});
```
兩者比較,重點就是 apollo 對於快取有比較多的操作
| **特性** | **react-query** | **apollo** |
|-------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| **快取操作方式** | 使用 `queryClient`,提供基本的讀取和寫入 API | 提供豐富的快取 API,例如 `readQuery`、`writeQuery` 等,允許精確操作查詢或片段資料 |
| **資料標準化** | 資料未標準化,每個 `queryKey` 的結果是獨立的,快取不會共享 | 資料標準化,基於 `__typename` 和 `id`,不同查詢可以共享相同資料 |
| **可讀性與靈活性** | 靈活,但需要手動處理快取的結構,對於複雜資料結構可能較麻煩 | 內建標準化與快取管理,適合處理複雜的 GraphQL 結構,但依賴 GraphQL 特性 |
| **修改特定快取資料** | 提供簡單的 `setQueryData` 方法,但需手動管理修改邏輯 | 提供針對查詢或片段的 `writeQuery`、`writeFragment`,支持精確控制快取資料 |
| **刪除快取資料** | 使用 `removeQueries` 或 `invalidateQueries`,簡單但需依賴 `queryKey` | 提供 `cache.evict` 和 `cache.gc` 方法,可精確刪除特定資料或進行全域快取清理 |
#### ♻️ 保存與回收
- **react-query**
data 的生命週期分為兩條線:
```
(1) [資料是否新鮮] fresh -> 超過 stale time 後 -> stale -> refetch
(2) [資料是否活躍] active -> 元件卸載 -> inactive -> 超過 cache time -> 銷毀
```

<small>來源:https://www.rootstrap.com/blog/react-query-and-management-of-server-state</small>
```javascript=
const { data } = useQuery(['todos'], fetchTodos, {
staleTime: 10 * (60 * 1000), // 10 mins
cacheTime: 15 * (60 * 1000), // 15 mins
});
queryClient.invalidateQueries(['todos']); // 觸發 refetch
queryClient.removeQueries(['todos']); // 移除快取
```
| 設定 | 預設值 | 描述 |
|--------------|-----------------------|----------------------------------------------|
| `staleTime` | `0` | 資料多久內不過期,預設為立即過期。 |
| `cacheTime` | `1000 * 60 * 5` (5 分鐘)| 快取多久會被刪除,應大於 staleTime。 |
👀 推薦閱讀:
- [React Query : staleTime vs cacheTime](https://dev.to/delisrey/react-query-staletime-vs-cachetime-hml)
- [React Query 的緩存機制 - stale time vs. cache (gc) time](https://alex-flow-state.netlify.app/react-js/react-query-cache/)
- [[npm] react-query | PJCHENder 未整理筆記](https://pjchender.dev/npm/npm-react-query/#staletime-vs-cachetime)
- **apollo**
- 手動清除 cache
```javascript=
const [deleteTodo] = useMutation(DELETE_TODO, {
update(cache, { data: { deleteTodo } }) {
cache.modify({
fields: {
todos(existingTodos = []) {
return existingTodos.filter(todo => todo.id !== deleteTodo.id);
},
},
});
},
});
```
```javascript=
cache.gc(); // 清理未被引用的快取
cache.evict({ fieldName: 'todos' }); // 移除特定字段的快取
```
💡 Apollo Client 的設計沒有 stale 機制 QQ,在 cache 中存太多資料後會生什麼事呢?
### 🪄 其他常用的 options
- react-query
- `refetchInterval`: 預設為 `false`,可以設定固定時間間隔 refetch
- `refetchIntervalInBackground`: 預設為 `false`,設為 `true` 即使視窗沒有 focus 也會觸發 refetch
- apollo
- `pollInterval`: 預設為 `0`,可以設定固定時間間隔 refetch
<!-- - 手動處理 refetch
```javascript=
const { refetch } = useQuery(GET_TODOS);
// 手動 refetch
button.onclick = () => refetch();
```-->
## 開發體驗
### 👩🏿💻 Dev Tool
- react-query
- npm 套件 @tanstack/react-query-devtools

<small>來源:https://blog.delpuppo.net/react-query-devtools</small>


<small>來源:https://daily-dev-tips.com/posts/make-your-life-easier-with-react-query-devtools/</small>
- apollo
- browser 套件 - [Apollo Client Devtools](https://github.com/apollographql/apollo-client-devtools) *稍微麻煩,不是 run 專案就能使用

<small>來源:https://chromewebstore.google.com/detail/apollo-client-devtools/jdkknkkbebbapilgoeccciglkfbmbnfm</small>
### 🫀 關注點
- **react-query**
- stale time 與 cache time 的設置與差異 (資料的生命週期)
- stale time:資料的保鮮期,控制資料是否需要重新抓取
- cache time:非活躍資料的保留期,決定快取資料何時被清理
- 主要針對 **每次 fetch 行為** 進行管理
👉 側重於 查詢行為(fetch-focused),資料管理以「查詢是否需要重新抓取」為核心
- **apollo**
<!-- - GraphQL 的語法(GQL syntax)與查詢策略(fetch policy) -->
- 快取資料的關聯結構(normalized cache),如何透過 `__typename` 與 `id` 管理共享資料
- 快取的操作方法 (ex: `cache.writeQuery`、`cache.readField`),需要熟悉快取內部的結構才能操作
- 需要考慮 資料欄位的一致性與同步性
👉 側重於 資料管理(cache-focused),以「快取資料的結構與共享」為核心
## 🍮 總結
apollo 比 react-query 多了 Normalized Data、Subscriptions (websocket),他的 cache 預設是永久的
react-query 開發時更注重每筆 fetch 的生命週期管理,並有 stale gc 機制,個人覺得較較單純
apollo 開發時更注重個欄位快取的結構與資料一致性,心智負擔較大 😵💫
#### 適用場景與特性整理
<!-- | 特性 | React Query | Apollo Client |
|---------------|-----------------------------------------|--------------------------------|
| 設計理念 | 通用查詢管理,支持任何資料來源 | 專為 GraphQL 設計 |
| 快取機制 | 不標準化,每個查詢獨立快取 | 標準化,所有資料共享 |
| 學習曲線 | 低,適合初學者 | 高,需熟悉 GraphQL | -->
| 適用場景 | react-query | apollo |
|-------------------|----------------------------------------|------------------------------|
| REST API | ✅ 完全支持 | ❌ 過於複雜,不建議 |
| GraphQL API | ✅ 支援,但不標準化快取 | ✅ 專為 GraphQL 設計 |
| 多數據來源 | ✅ 非常靈活 | ⚠️ 支援,但需額外處理 |
| 即時資料更新 | ⚠️ 基本支持(手動 WebSocket 實現) | ✅ 支援 GraphQL Subscriptions |
| 本地狀態管理 | ❌ 無 | ✅ 可將本地狀態存入快取 |
| 特性 | react-query | apollo |
|-------------------------------|--------------------------------|-----------------------------|
| **靈活性** | 高,適合 REST 與 GraphQL 混合架構 | 偏向 GraphQL-first 的應用 |
| **標準化快取** | 無 | 內建,適合共享資料場景 |
| **GraphQL 特性支援** | 基礎支援,需額外手動實現 | 完整支援,包括 Subscriptions |
| **學習曲線** | 較低,容易上手 | 較高,需熟悉 GraphQL 特性 |
| **性能與體積** | 小且快,約 12 KB | 功能完整但較大,約 38 KB |
#### 🧑🏫 最後再複習一下完整的 code
- **react-query**
```javascript=
function Todos() {
const queryClient = useQueryClient();
const { data: todos, isLoading, isError } = useQuery(['todos'], fetchTodos);
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => queryClient.invalidateQueries(['todos']),
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading todos</div>;
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button onClick={() => mutation.mutate({ title: 'New Todo', completed: false })}>
Add Todo
</button>
</div>
);
}
```
- **apollo**
```javascript=
function Todos() {
const { data, loading, error } = useQuery(GET_TODOS);
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{ query: GET_TODOS }],
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error loading todos</div>;
return (
<div>
<ul>
{data.todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button onClick={() => addTodo({ variables: { title: 'New Todo', completed: false } })}>
Add Todo
</button>
</div>
);
}
```
---
關於目前專案 Apollo 的延伸討論
1. 如果想使用 codegen 連至 staging 拿 schema (?) 身份驗證會碰到什麼問題嗎?>> 後端需要上 schema 到 staging,前端再研究一下套件的實用性,有需要再請後端寫著
2. Cache id 有沒有什麼要注意的?
3. Error Handler?
👋