使用 placeholderData 與 keepPreviousData 提升分頁體驗
不喜歡看字的可以看影片:
- Vue3 教學 - Vue Query Part.1 使用 useQuery 與 useMutation
- Vue3 教學 - Vue Query Part.2 資料生命週期與重新獲取機制 (stale,fresh,paused,inActive)
- Vue3 教學 - Vue Query Part.3 使用 placeholderData 與 keepPreviousData 提升分頁體驗
- Vue3 教學 - Vue Query Part.4 使用 useMutation 進行樂觀更新
- Vue3 教學 - Vue Query Part.5 如何使用 enabled 控制查詢與 prefetch 提升使用者體驗
簡介
在開發分頁 API 時,使用者在切換頁數時 可能會遇到閃爍,因為:
- API 請求回應有延遲,導致畫面會短暫沒有資料 (白屏)。
- 切換頁面時資料會瞬間消失,影響使用者體驗。
Vue Query 提供了 placeholderData
和 keepPreviousData
兩種方式,來讓使用者在分頁時有更順暢的體驗:
placeholderData
:在新數據載入前,先顯示預設數據 (例如上一頁的數據)。keepPreviousData
:當 API 正在加載時,維持前一頁的數據,避免畫面閃爍。
設定 JSON Server 作為測試 API
如果還沒安裝 JSON Server 的,可以參考一下影片。
npx json-server --watch db.json --port 3002
然後在 db.json
中新增幾筆 todos
資料:
{
"todos": [
{ "id": 1, "title": "待辦事項 1" },
{ "id": 2, "title": "待辦事項 2" },
{ "id": 3, "title": "待辦事項 3" },
{ "id": 4, "title": "待辦事項 4" },
{ "id": 5, "title": "待辦事項 5" },
{ "id": 6, "title": "待辦事項 6" },
{ "id": 7, "title": "待辦事項 7" },
{ "id": 8, "title": "待辦事項 8" },
{ "id": 9, "title": "待辦事項 9" },
{ "id": 10, "title": "待辦事項 10" }
]
}
現在我們可以透過 http://localhost:3002/todos?_page=1&_limit=5
來取得分頁資料。
不使用 placeholderData 與 keepPreviousData 的狀況
現在先來看看不使用 placeholderData
與 keepPreviousData
的狀況,將以下程式碼複製到 Todos.vue
中:
Todos.vue
<script setup>
import { ref } from "vue";
import { useQuery } from "@tanstack/vue-query";
import axios from "axios";
const page = ref(1);
const limit = 3; // 每頁 3 筆
const fetchTodos = async ({ queryKey }) => {
const [_key, page] = queryKey;
const { data } = await axios.get(`http://localhost:3002/todos?_page=${page}&_limit=${limit}`);
return data;
};
const { data, isLoading, isFetching } = useQuery({
queryKey: ["todos", page],
queryFn: fetchTodos,
});
const nextPage = () => {
if (!isFetching.value) page.value++;
};
const prevPage = () => {
if (page.value > 1 && !isFetching.value) page.value--;
};
</script>
<template>
<div>
<h2>待辦事項 (分頁 API)</h2>
<p v-if="isLoading">載入中...</p>
<ul>
<li v-for="todo in data" :key="todo.id">{{ todo.title }}</li>
</ul>
<button @click="prevPage" :disabled="page === 1">上一頁</button>
<button @click="nextPage" :disabled="isFetching">下一頁</button>
<p v-if="isFetching">載入新頁面資料中...</p>
</div>
</template>
然後按下 F12 開啟 DevTools,把 Network
的 Throttle
調整為 3G
,這樣可以模擬網路較差時的狀況,也就可以看到當我們按下 下一頁
切換頁面時,整個當前渲染的資料會消失,直到資料載入完成後,才會重新渲染新的資料。
使用 placeholderData 與 keepPreviousData 提升分頁體驗
現在我們來看看使用 placeholderData
與 keepPreviousData
的狀況,使用的方式很簡單,只要在 useQuery
的 options
中加入 placeholderData
與 keepPreviousData
即可,將以下程式碼複製到 Todos.vue
中:
Todos.vue
<script setup>
import { ref } from "vue";
import { useQuery, keepPreviousData } from "@tanstack/vue-query";
import axios from "axios";
const page = ref(1);
const limit = 3; // 每頁 3 筆
const fetchTodos = async ({ queryKey }) => {
const [_key, page] = queryKey;
const { data } = await axios.get(`http://localhost:3002/todos?_page=${page}&_limit=${limit}`);
return data;
};
const { data, isLoading, isFetching } = useQuery({
queryKey: ["todos", page],
queryFn: fetchTodos,
placeholderData: keepPreviousData,
});
const nextPage = () => {
if (!isFetching.value) page.value++;
};
const prevPage = () => {
if (page.value > 1 && !isFetching.value) page.value--;
};
</script>
<template>
<div>
<h2>待辦事項 (分頁 API)</h2>
<p v-if="isLoading">載入中...</p>
<ul>
<li v-for="todo in data" :key="todo.id">{{ todo.title }}</li>
</ul>
<button @click="prevPage" :disabled="page === 1">上一頁</button>
<button @click="nextPage" :disabled="isFetching">下一頁</button>
<p v-if="isFetching">載入新頁面資料中...</p>
</div>
</template>
注意事項
當我們使用了 placeholderData
與 keepPreviousData
時,isLoading
只有一開始會是 true
,當資料載入完成後,isLoading
會變成 false
,即使我們切換頁面時,isLoading
也不會變成 true
,這時候我們可以透過 isPlaceholderData
來取代 isLoading
,來達到更好的體驗。
Todos.vue
<script setup>
import { ref } from "vue";
import { useQuery, keepPreviousData } from "@tanstack/vue-query";
import axios from "axios";
const page = ref(1);
const limit = 3; // 每頁 3 筆
const fetchTodos = async ({ queryKey }) => {
const [_key, page] = queryKey;
const { data } = await axios.get(`http://localhost:3002/todos?_page=${page}&_limit=${limit}`);
return data;
};
const { data, isLoading, isFetching, isPlaceholderData } = useQuery({
queryKey: ["todos", page],
queryFn: fetchTodos,
placeholderData: keepPreviousData,
});
const nextPage = () => {
if (!isFetching.value) page.value++;
};
const prevPage = () => {
if (page.value > 1 && !isFetching.value) page.value--;
};
</script>
<template>
<div>
<h2>待辦事項 (分頁 API)</h2>
<!-- 換成 isPlaceholderData 來取代 isLoading -->
<p v-if="isPlaceholderData">載入中...</p>
<ul>
<li v-for="todo in data" :key="todo.id">{{ todo.title }}</li>
</ul>
<button @click="prevPage" :disabled="page === 1">上一頁</button>
<button @click="nextPage" :disabled="isFetching">下一頁</button>
<p v-if="isFetching">載入新頁面資料中...</p>
</div>
</template>