📘 PHẦN 3: CÁC PHƯƠNG PHÁP QUẢN LÝ TRẠNG THÁI (VUEX & PINIA)
🎯 Mục tiêu tổng quát
- Hiểu rõ các vấn đề phát sinh khi quản lý trạng thái trong ứng dụng lớn.
- Nắm vững các khái niệm cốt lõi của Vuex: State, Getters, Mutations, và Actions.
- Nắm vững các khái niệm cốt lõi của Pinia: State, Getters, và Actions.
- Phân biệt được sự khác biệt về triết lý và cú pháp giữa Vuex và Pinia, và biết khi nào nên chọn thư viện nào.
- Xây dựng một store tập trung để quản lý trạng thái giỏ hàng bằng Pinia (giải pháp được khuyến khích cho dự án mới).
- Sử dụng
slotsđể tạo ra các component layout linh hoạt.
🧑🏫 Bài 1: Vấn đề của việc quản lý trạng thái
Prop Drilling và Event Bubbling
Khi ứng dụng lớn dần, việc truyền state và sự kiện qua nhiều cấp component trở nên rất cồng kềnh, khó bảo trì và dễ gây lỗi. Vấn đề này được giải quyết bằng các thư viện quản lý trạng thái tập trung, tạo ra một "kho" (store) toàn cục mà bất kỳ component nào cũng có thể truy cập.
🧑🏫 Bài 2: Vuex - Người tiền nhiệm mạnh mẽ
Vuex là gì?
Vuex là thư viện quản lý trạng thái chính thức cho Vue 2. Nó hoạt động dựa trên một store duy nhất (single source of truth) và một luồng dữ liệu nghiêm ngặt.
Bốn khái niệm cốt lõi: State, Getters, Mutations, Actions
- State: Tương tự như
data. Là nơi chứa dữ liệu của toàn bộ ứng dụng. - Getters: Tương tự như
computed. Dùng để dẫn xuất dữ liệu từ state. - Mutations: Cách duy nhất để thay đổi
statemột cách đồng bộ. Đây là một điểm khác biệt lớn so với Pinia. Mỗi mutation là một hàm nhậnstatelàm tham số. - Actions: Tương tự như
methods. Dùng để thực thi logic nghiệp vụ, đặc biệt là các thao tác bất đồng bộ (như gọi API). Actions không trực tiếp thay đổi state, thay vào đó, chúngcommitcác mutations.
Luồng dữ liệu trong Vuex:Component --(dispatch)--> Action --(commit)--> Mutation --(thay đổi)--> State --(render)--> Component
Ví dụ: Xây dựng Store với Vuex
Tạo file src/store/index.js
// src/store/index.js (Cú pháp Vuex 4 cho Vue 3)
import { createStore } from 'vuex';
export default createStore({
// 1. STATE
state: {
cart: {
items: [],
},
},
// 2. GETTERS
getters: {
cartItemCount(state) {
return state.cart.items.length;
},
},
// 3. MUTATIONS (phải là đồng bộ)
mutations: {
ADD_ITEM_TO_CART(state, product) {
state.cart.items.push(product);
},
},
// 4. ACTIONS (có thể là bất đồng bộ)
actions: {
// context object chứa { commit, state, getters, dispatch }
addProductToCart(context, product) {
// Có thể thực hiện logic phức tạp hoặc gọi API ở đây
// Sau đó commit mutation để thay đổi state
context.commit('ADD_ITEM_TO_CART', product);
},
},
});Sử dụng Vuex Store trong Components
Trong <script setup>, bạn có thể sử dụng hook useStore.
<script setup>
import { useStore } from 'vuex';
import { computed } from 'vue';
const props = defineProps(['product']);
const store = useStore(); // Lấy store
// Lấy state và getter (nên dùng computed để giữ reactivity)
const itemCount = computed(() => store.getters.cartItemCount);
// Gọi action
function handleAddToCart() {
store.dispatch('addProductToCart', props.product);
}
</script>
<template>
<div>Sản phẩm trong giỏ: {{ itemCount }}</div>
<button @click="handleAddToCart">Thêm vào giỏ</button>
</template>🧑🏫 Bài 3: Pinia - Giải pháp hiện đại
Pinia là gì và tại sao nó ra đời?
Pinia là thư viện quản lý trạng thái chính thức mới cho Vue, được thiết kế để đơn giản và trực quan hơn Vuex. Nó loại bỏ khái niệm mutations và cho phép bạn tổ chức store thành nhiều module nhỏ (thay vì một store khổng lồ).
So sánh Pinia và Vuex
| Tính năng | Vuex | Pinia |
|---|---|---|
| Mutations | Bắt buộc. Cách duy nhất để thay đổi state. | Không có. Actions có thể thay đổi state trực tiếp. |
| Actions | Gọi qua dispatch('actionName'). | Gọi như một phương thức bình thường: store.actionName(). |
| Modules | Hỗ trợ, nhưng cú pháp phức tạp. | Mặc định. Mỗi store là một module. |
| TypeScript | Hỗ trợ cơ bản. | Hỗ trợ tuyệt vời, tự động suy luận kiểu. |
| Tổng quan | Nghiêm ngặt, cấu trúc rõ ràng. | Linh hoạt, đơn giản, ít boilerplate. |
Khuyến nghị:
- Sử dụng Pinia cho tất cả các dự án Vue 3 mới.
- Học Vuex để có thể làm việc và bảo trì các dự án Vue 2 hoặc các dự án Vue 3 cũ hơn.
Ví dụ: Xây dựng Store với Pinia
Tạo file src/stores/cart.js
// src/stores/cart.js
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
// 1. STATE
state: () => ({
items: [],
}),
// 2. GETTERS
getters: {
itemCount: (state) => state.items.length,
},
// 3. ACTIONS (thay đổi state trực tiếp)
actions: {
addItem(product) {
this.items.push(product); // Đơn giản hơn nhiều!
},
},
});Sử dụng Pinia Store trong Components
<script setup>
import { useCartStore } from '../stores/cart';
const props = defineProps(['product']);
const cartStore = useCartStore(); // Lấy store
function handleAddToCart() {
cartStore.addItem(props.product); // Gọi action trực tiếp
}
</script>
<template>
<!-- Truy cập state/getter trực tiếp từ store instance -->
<div>Sản phẩm trong giỏ: {{ cartStore.itemCount }}</div>
<button @click="handleAddToCart">Thêm vào giỏ</button>
</template>🧑🏫 Bài 4: Slots - Tạo Component Layout linh hoạt
Slots là một cơ chế cho phép bạn truyền các khối template từ component cha vào các "khe" được định nghĩa sẵn trong component con, giúp component con trở nên cực kỳ linh hoạt và tái sử dụng được.
Slots cơ bản, Named Slots, và Scoped Slots
- Slots cơ bản (
<slot>): Một khe duy nhất để chèn nội dung. - Named Slots (
<slot name="header">): Nhiều khe được đặt tên để chèn nội dung vào các vị trí cụ thể. - Scoped Slots: Cho phép component con truyền dữ liệu ngược lại cho nội dung của slot ở component cha.
🧪 BÀI TẬP LỚN CUỐI PHẦN: Quản lý giỏ hàng "SimpleStore" bằng Pinia
Mô tả bài toán
Tái cấu trúc lại toàn bộ logic quản lý giỏ hàng của "SimpleStore", chuyển nó từ state cục bộ trong App.vue sang một store tập trung bằng Pinia, vì đây là giải pháp hiện đại được khuyến khích.
Yêu cầu
- Cài đặt Pinia: Cài đặt (
npm install pinia) và thiết lập Pinia cho dự án trongsrc/main.js. - Tạo Cart Store:
- Tạo file
src/stores/cart.js. - Định nghĩa một store
useCartStorevới các phần sau:- State:
items(mảng rỗng). - Getters:
itemCount(tính độ dài mảngitems) vàtotalPrice(tính tổng giá trị giỏ hàng). - Actions:
addItem(product)(nếu sản phẩm đã có thì tăng quantity, nếu không thì thêm mới) vàremoveItem(productId).
- State:
- Tạo file
- Tái cấu trúc Components:
AppHeader.vue: Sử dụnguseCartStoređể lấy và hiển thịitemCount.ProductCard.vue: Xóaemit. Thay vào đó, khi click nút "Thêm vào giỏ", gọi trực tiếp actioncartStore.addItem().App.vue: Xóa toàn bộ state và logic liên quan đến giỏ hàng.
- Tạo component Layout:
- Tạo một component
src/components/BaseLayout.vuesử dụng named slots (#header,#default,#footer). - Trong
App.vue, sử dụngBaseLayoutđể cấu trúc trang, đặtAppHeadervào slotheader.
- Tạo một component
- Tạo trang giỏ hàng (mô phỏng):
- Tạo một component mới
src/components/ShoppingCart.vue. - Component này sẽ sử dụng
useCartStoređể hiển thị danh sách sản phẩm trong giỏ, tổng tiền, và các nút để gọi actionremoveItem. - Sử dụng
ShoppingCart.vuetrongApp.vue.
- Tạo một component mới
(Tùy chọn) Thử thách thêm
- Sau khi hoàn thành bài tập với Pinia, hãy thử tạo một nhánh git mới và làm lại bài tập tương tự nhưng sử dụng Vuex. Điều này sẽ giúp bạn củng cố sự khác biệt giữa hai thư viện.
Mục tiêu: Kết thúc phần này, bạn không chỉ xây dựng được một hệ thống quản lý trạng thái mạnh mẽ, mà còn hiểu rõ hai công cụ phổ biến nhất trong hệ sinh thái Vue, sẵn sàng để lựa chọn giải pháp phù hợp cho các dự án trong tương lai.
