# 🏅 Day 20 - Nuxt3 狀態管理 - Pinia - ( 2 )
## 今日學習目標
- 學習如何在 Nuxt3 中操作 Pinia,並結合伺服器端的狀態管理
- 學習如何在 API 請求之後,將取得的資料同步至 Pinia
- 理解伺服器端與客戶端操作 Pinia 的差異
## 前言
在 Day19 的學習範例中,我們使用 Pinia 建立了一個用於管理待辦事項列表 ( todolist ) 的 store ,並在客戶端實作新增、刪除待辦事項的功能。今天我們將學習如何透過 API 來取得、新增和刪除待辦事項,並使用 Pinia 來管理和同步這些資料狀態。
### 範例準備
今天將使用以下三支 API 進行講解,在操作待辦事項之前,需要先註冊並登入 TodoList 的帳號 。 請求標頭、帶入資料與成功、失敗的訊息格式可以閱讀 [API 文件](https://todolist-api.hexschool.io/doc/#/%E4%BD%BF%E7%94%A8%E8%80%85) 。
- 取得待辦事項功能 :
```
https://todolist-api.hexschool.io/todos/
method:'GET'
// 請求標頭
headers:{
Authorization // 需帶入 token
}
```
- 新增待辦事項功能 :
```
https://todolist-api.hexschool.io/todos/
method:'POST'
// 請求標頭
headers:{
Authorization // 需帶入 token
}
請求資料格式:
{
"content": "事項"
}
```
- 刪除待辦事項功能 :
```
https://todolist-api.hexschool.io/todos/{id} // 傳入 todo 的 id
methods:'DELETE'
// 請求標頭
headers:{
Authorization // 需帶入 token
}
```
## 在伺服器端與客戶端操作 Pinia
### 伺服器端
Nuxt 初始化時,Pinia 的 store 會在伺服器端被初始化。在伺服器端可以預先發送 API 請求並將取得的資料存入 store 。伺服器端初始化完成後,store 的狀態會與 HTML 一起傳遞至客戶端,這樣客戶端在首次載入時可以直接從 store 中取出資料。
在伺服器端可以透過 `useFetch` 或 `useAsyncData` 發送 API 請求並將回應結果存入 Pinia。例如下方結構使用了 `useFetch` 在頁面中取得任務列表的資料,並將 `tasks`同步至 `taskStore`。使用 `useFetch` 可以在伺服器端預先取得資料,但是傳遞至客戶端會再重複執行一次,將 `tasks.value` 資料寫入 `taskStore.taskList` 的動作。
```html
// 伺服器端執行:
1. 取出 useTaskStore()
2. await useFetch 發出 GET 請求並回傳資料。回傳的資料也會一起被傳遞至客戶端
3. 把 tasks 列表資料寫入 useTaskStore
4. 將 taskStore.taskList 渲染至 HTML 元素 ( 完成伺服器端渲染 )
// 客戶端執行:
1. 取出 useTaskStore()
2. 把 tasks列表資料寫入 useTaskStore
3. 將 taskStore.todos 渲染至 HTML 元素
<script setup>
const taskStore = useTaskStore();
const { data: tasks } = await useFetch('https://example.com/tasks/', {
headers: { Authorization },
});
// 將資料同步到 Pinia 中
taskStore.taskList= tasks.value;
</script>
<template>
{{ taskStore.taskList }}
</template>
```
為了避免 `useFetch()` 在客戶端重複把 `tasks.value` 資料寫入 `taskStore.taskList`,可以改用 Pinia 官方 [建議的寫法](https://pinia.vuejs.org/ssr/nuxt.html#Awaiting-for-actions-in-pages) ,以 `useAsyncData`在伺服器端執行 `$fetch` 送出請求。`$fetch` 請求回傳後立刻將資料寫入伺服器端的 `useTaskStore`,而在客戶端只需直接取用伺服器端的資料。
```html
// 伺服器端執行:
1. 取出 useTaskStore()
2. await useAsyncData 發出 GET 請求,把 tasks 列表資料寫入 useTaskStore
3. 將 taskStore.todos 渲染至 HTML 元素 ( 完成伺服器端渲染 )
// 客戶端執行:
1. 取出 useTaskStore()
2. 將 taskStore.taskList 渲染至模板
<script setup>
const taskStore = useTaskStore();
// 注意: 這邊不解構回傳的 data
await useAsyncData('tasks', () => {
const tasks = $fetch('https://example.com/tasks/', {
headers: { Authorization },
})
// 在伺服器端執行,將資料寫入伺服器端的 useTaskStore
taskStore.taskList = tasks;
return tasks;
});
</script>
<template>
{{ taskStore.taskList }}
</template>
```
### 客戶端
伺服器端初始化完畢,Pinia 的資料狀態將被傳遞至客戶端,客戶端可以繼續使用 Pinia store 的狀態。在客戶端,可以使用 `$fetch` 發送 API 請求,並在 Pinia store 中更新狀態。例如下方結構,在客戶端透過點擊事件新增代辦事項時,可以在 `$fetch` 發送請求之後將回傳的資料更新至`taskStore`。
```jsx
<script setup>
const taskStore = useTaskStore();
const addTask = async (content) => {
const newTask = await $fetch('https://example.com/tasks/', {
method: 'POST',
headers: { Authorization },
body: { content },
});
// 更新 Pinia store
taskStore.taskList.push(newTask);
};
</script>
```
## 待辦事項功能實作
在開始實作代辦事項之間,需要先註冊並登入一個帳號再操作哦 !
註冊和登入的功能已在今日講解範例的 `/pages/index.vue` 中提供。
### 取得全部待辦事項
在 `stores/todo.js` 中設定 `getTodo` 方法,透過 `useAsyncData` 在伺服器端初始化待辦事項列表。當 `$fetch` 請求回傳後, 將待辦事項列表存入 `todoList`。
```jsx
// stores/todo.js
const todoList = ref([]);
const isFetch = ref(false);
// 在伺服器端取得 todo 列表
const getTodo = async (token) => {
await useAsyncData("getTodoList", async () => {
const { data } = await $fetch(
"https://todolist-api.hexschool.io/todos/",
{
headers: { Authorization: token },
}
);
todoList.value = data;
return data;
});
};
```
接著,在 `/pages/todo.vue` 中使用 `await useAsyncData()` 執行 `getTodo`,並將從 cookie 取得的 token 傳入,以便伺服器端初始化資料。
```html
<!-- /pages/todo.vue -->
<script setup>
const cookie = useCookie("auth");
const todoStore = useTodoStore();
const { getTodo } = todoStore;
// 在伺服器端初始化資料,傳遞給客戶端。
await useAsyncData("todos", () => getTodo(cookie.value).then(() => true));
</script>
```
### 新增待辦事項
在 `stores/todo.js` 中設定 `addTodo` 方法。由於新增待辦事項是由使用者在客戶端點擊觸發,因此
請求需透過 `$fetch` 發出 。請求成功後,`$fetch` 會回傳新增的待辦事項 `newTodo`, 可以直接以 `push()` 的方式更新至 `todoList`。
```jsx
// stores/todo.js
const todoList = ref([]);
const isFetch = ref(false);
// 新增待辦事項 ( 客戶端操作 )
const addTodo = async (content, token) => {
isFetch.value = true;
try {
const { newTodo } = await $fetch(
"https://todolist-api.hexschool.io/todos/",
{
method: "POST",
headers: { Authorization: token },
body: { content },
}
);
/*
newTodo 為新增的待辦事項,格式如下
{
"id",
"createTime,
"content",
"status":
}
*/
todoList.value.push(newTodo);
} catch (error) {
console.log(error);
} finally {
isFetch.value = false;
}
};
```
```html
<!-- /pages/todo.vue -->
<script setup>
const cookie = useCookie("auth");
const todoStore = useTodoStore();
const { addTodo } = todoStore;
const newTodo = ref("");
// 點擊後,透過 todoStore 的 addTodo 方法將新增代辦事項
const addNewTodo = () => {
if (newTodo.value) {
addTodo(newTodo.value, cookie.value);
newTodo.value = "";
}
};
</script>
<template>
<div class="input-group mb-3">
<input
v-model.trim="newTodo"
class="form-control"
placeholder="新增待辦事項"
/>
<button
@click="addNewTodo"
class="btn btn-primary"
:disabled="!newTodo.length || isFetch"
>
新增 Todo
</button>
</div>
</template>
```
需要注意,如果將 `todoList.value.push(newTodo);`的資料更新方式替換為 `stores/todo.js` 中的 `getTodo` 方法,在瀏覽器 console 會發生下圖 `[nuxt] [useAsyncData] Component is already mounted, please use $fetch instead.`的警告。

造成警告的原因是 `getTodo` 方法內部使用了 `useAsyncData`,而 `useAsyncData` 主要用於伺服器端渲染,在元件初始化時加載資料。當元件已經掛載到客戶端後,繼續使用 `useAsyncData` 進行資料請求會觸發此警告。如果需要在客戶端進行資料請求,建議改用 `$fetch`,不過在這個範例的做法可以改用陣列 `push()` 方法將新增的資料更新至 `todoList` 資料,減少請求 API 的次數。
```jsx
// 新增待辦事項 ( 客戶端操作 )
const addTodo = async (content, token) => {
isFetch.value = true;
try {
const { newTodo } = await $fetch(
"https://todolist-api.hexschool.io/todos/",
{
method: "POST",
headers: { Authorization: token },
body: { content },
}
);
// await getTodo(token);
// 因為不應在客戶端操作 useAsyncData,所以不使用 getTodo 方法更新 todoList 資料
// 改成使用陣列 push() 更新資料來以減少 API 請求次數
todoList.value.push(newTodo);
} catch (error) {
console.dir(error);
} finally {
isFetch.value = false;
}
};
```
### 刪除待辦事項
在 `stores/todo.js` 中設定 `removeTodo`方法。使用者點擊刪除按鈕後,將待辦事項的 `id` 與 `token` 傳入`$fetch` 發出 `DELETE` 請求。請求成功後,透過待辦事項的 `id` 將該筆事項從 `todoList` 刪除。
```jsx
// stores/todo.js
const todoList = ref([]);
const isFetch = ref(false);
// 刪除待辦事項 ( 客戶端操作 )
const removeTodo = async (id, cookie) => {
const todoIndex = todoList.value.findIndex((todo) => todo.id === id);
isFetch.value = true;
try {
await $fetch(`https://todolist-api.hexschool.io/todos/${id}`, {
method: "DELETE",
headers: { Authorization: cookie },
});
todoList.value.splice(todoIndex, 1);
} catch (error) {
console.log(error);
} finally {
isFetch.value = false;
}
};
```
```html
<!-- /pages/todo.vue -->
<script setup>
const cookie = useCookie("auth");
const todoStore = useTodoStore();
const { removeTodo } = todoStore;
// 移除 Todo
const deleteTodo = (id) => {
removeTodo(id, cookie.value);
};
</script>
<template>
<ul class="list-group">
<li
v-for="todo in todoList"
:key="todo.id"
class="list-group-item d-flex justify-content-between align-items-center"
>
<span>
{{ todo.content }}
</span>
<button
class="btn btn-danger btn-sm"
@click="deleteTodo(todo.id)" <=== 在客戶端,透過點擊事件處發刪除請求
:disabled="isFetch"
>
刪除
</button>
</li>
</ul>
</template>
```
<br>
> 今日學習的[範例 Code - 資料夾: day20-pinia-api-example](https://github.com/hexschool/nuxt-daily-tasks-2024)
## 題目
根據最終任務訂房網站 [設計稿](https://www.figma.com/design/6pTFrdb5a1lYKmMnFeT5Mf/%E5%85%AD%E8%A7%92-Project-%2F-%E9%85%92%E5%BA%97%E8%A8%82%E6%88%BF%E7%B6%B2%E7%AB%99?t=wmvFC4GdPPgblvwT-0) ,在訂房流程中,當使用者在「房型詳細頁」點擊「立即預訂」按鈕後需呈現該房型的詳細資訊。因此需要將房型詳細資料加入全域狀態管理。
請 fork 這一份 [模板](https://github.com/jasonlu0525/nuxt3-live-question/tree/day20-pinia-api) ,完成以下條件 :
- 在 `/pages/room.vue` 的房型列表中,點擊房型進入房型詳細頁(`/pages/room/[id].vue`),將取得的房型資料寫入 Pinia Store ,並渲染於頁面模板。
```jsx
// 將房型資料 data 改成使用 Pinia 管理
const { data, error } = await useAsyncData(`room-data`, async () => {
const response = await $fetch(`/rooms/${id}`, {
baseURL: "https://nuxr3.zeabur.app/api/v1",
});
return response.result;
});
```
- 在房型詳細頁點擊「立即預訂」按鈕後,導向 `/pages/booking.vue` 預約頁面。在此頁面中,將房型資料從 Pinia Store 取出,渲染於頁面模板。
```jsx
// /pages/booking.vue
<script setup>
// 將 bookingInfo 改成使用 Pinia 的資料
const bookingInfo = ref({});
</script>
```
- 補充,Pinia Store 的檔案已於 `stores/booking.js` 提供,請將匯出的 Store 名稱命名為 `useBookingStore` 。
```jsx
/stores/booking.js
// export const useBookingStore =
```
## 回報流程
將答案上傳至 GitHub 並複製 GitHub repo 連結貼至底下回報就算完成了喔 !
解答位置請參考下圖(需打開程式碼的部分觀看)

<!--
解答 : https://github.com/jasonlu0525/nuxt3-live-answer/tree/day20-pinia-api
-->
回報區
---
| # | Discord | Github / 答案 |
| --- | ----- | ----- |
|1|眼睛|[Github](https://github.com/Thrizzacode/nuxt3-live-question/tree/day20-pinia-api)|
|2|Steven|[Github](https://github.com/y7516552/nuxt3-live-question/tree/day20)|
|3|dragon|[Github](https://github.com/peterlife0617/2024-nuxt-training-homework01/tree/feature/day20)|
| 4 | Rocky |[Github](https://github.com/WuRocky/Nuxt-Day20-Pinia-2.git)|
| 5 | LinaChen |[Github](https://github.com/Lina-SHU/nuxt3-live-question)|
| 6 | Johnson |[Github](https://github.com/tttom3669/2024_hex_nuxt_daily/tree/day20-pinia-api)|
| 7 | JimLin |[Github](https://github.com/junhoulin/Nuxt3-hw-day-after10/tree/day20)|
| 8 | tanuki狸 |[Github](https://github.com/tanukili/Nuxt-2024-week01-2/tree/day20-pinia-api)|
|9|Ariel|[Github](https://github.com/Ariel0508/nuxt3-hw/tree/day20-pinia-api)|
|10|hsin yu|[Github](https://github.com/dogwantfly/nuxt3-daily-task-live-question/tree/day20-pinia-api)|
<!--
|---|---|[Github]()|
-->