Tiếp tục với Bài 27: Tạo hiệu ứng Loading toàn cục và xử lý trạng thái chờ khi tải dữ liệu, đây là kỹ năng quan trọng giúp người dùng biết rằng hệ thống đang xử lý – tránh cảm giác “đơ”, “không phản hồi”.
Mục tiêu
- Tạo store quản lý loading toàn app
- Hiển thị overlay hoặc spinner khi đang xử lý
- Tích hợp loading khi gọi API, khi chuyển trang
- Có thể mở rộng thành loading theo khu vực (per-component)
Phần 1: Tạo loadingStore
bằng Pinia
Tạo src/store/loadingStore.js
:
import { defineStore } from 'pinia'
export const useLoadingStore = defineStore('loading', {
state: () => ({
isLoading: false
}),
actions: {
start() {
this.isLoading = true
},
stop() {
this.isLoading = false
}
}
})
Phần 2: Tạo component GlobalLoading.vue
<template>
<div v-if="isLoading" class="overlay">
<div class="spinner"></div>
</div>
</template>
<script setup>
import { useLoadingStore } from '@/store/loadingStore'
import { storeToRefs } from 'pinia'
const loadingStore = useLoadingStore()
const { isLoading } = storeToRefs(loadingStore)
</script>
<style scoped>
.overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.spinner {
border: 4px solid #ccc;
border-top-color: #2c3e50;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
Phần 3: Hiển thị trong toàn bộ app
Trong App.vue
:
<template>
<router-view />
<GlobalLoading />
</template>
<script setup>
import GlobalLoading from './components/GlobalLoading.vue'
</script>
Phần 4: Gọi loading khi fetch dữ liệu
Ví dụ trong todoStore.js
:
import { useLoadingStore } from './loadingStore'
const loading = useLoadingStore()
async function fetchTodos() {
loading.start()
try {
const res = await fetch('http://localhost:5000/todos', {
headers: {
Authorization: 'Bearer ' + localStorage.getItem('token')
}
})
const data = await res.json()
this.todos = data
} finally {
loading.stop()
}
}
Phần 5: Loading khi chuyển route
Trong src/router/index.js
:
import { useLoadingStore } from '@/store/loadingStore'
router.beforeEach((to, from, next) => {
const loading = useLoadingStore()
loading.start()
next()
})
router.afterEach(() => {
const loading = useLoadingStore()
setTimeout(() => loading.stop(), 300) // thêm delay nhỏ để mượt
})
Lưu ý: nếu dùng SSR thì nên dùng isClient
check để tránh lỗi.
Bạn đã học được
- Tạo loading toàn cục dùng Pinia
- Hiển thị overlay loading mượt mà
- Kết hợp loading khi gọi API và khi chuyển route
- Trải nghiệm người dùng rõ ràng, mượt, chuyên nghiệp hơn
Bài tập mở rộng
- Thêm kiểu loading dạng bar thay vì spinner
- Loading riêng từng component (ví dụ: loading trong bảng Todo)
- Kết hợp thư viện như nprogress để hiển thị loading trên đầu trang
Thảo luận