---
# System prepended metadata

title: "\U0001F3C5 Day 20"
tags: [2024 Nuxt 直播班]

---

# 🏅 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.`的警告。

![day20-1](https://hackmd.io/_uploads/SkJEjapzJx.png)


造成警告的原因是 `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://i.imgur.com/vftL5i0.png)

<!--   

解答 :  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]()|
 -->