Giới thiệu về Vuex

VueJS Jun 25, 2021

Giống như React, Angular một ứng dụng Vuejs cũng được xây dựng xung quanh các component. Khi ứng dụng có nhiều component, việc quản lý và chia sẻ dữ liệu giữa các component sẽ trở lên phức tạp, ví dụ sau khi login chúng ta sẽ có thông tin user và chúng ta muốn khi vào các page product, category cũng sẽ đều có thông tin user hay khi ấn add to cart, icon giỏ hàng trên navbar sẽ hiển thị số sản phẩm đã thêm vào giỏ hàng. Ta có thể sử dụng prop, emit, eventBus để truyền dữ liệu giữa các component, nhưng khi có quá nhiều component lồng nhau hoặc các component không có quan hệ với nhau việc này sẽ trở lên khó khăn. Vì vậy các thư viện quản lý state ra đời giúp quản lý và chia sẻ state giữa các component. Giống như React có redux, Flutter có Bloc, Getx, Provider thì Vue có Vuex.


1. Tổng quan về Vuex

Vuex là thư viện giúp quản lý state của các component trong Vue.js, nó là nơi lưu trữ tập trung cho tất cả các component trong một ứng dụng, với nguyên tắc state chỉ có thể được thay đổi theo kiểu có thể dự đoán.

Vuex đóng vai trò là một centralized state management. Vuex quản lý state thông qua một đối tượng global gọi là store, bên trong store gồm 4 thành phần có quan hệ chặt chẽ với nhau đó là state, getter, mutation, action.

2. Các thành phần trong Vuex

Store

Store là phần quản lí State, có các phương thức cho phép thay đổi state một cách gian tiếp thông qua dispatch hoặc một commit. Store là duy nhất bên trong một app và sẽ được khởi tạo cùng với root.

State

State là một object chứa các dữ liệu chúng ta muốn chia sẻ trong app.

Getters

Getters là một computed dùng để lấy và lọc data theo một yêu cầu nào đó mà nhiều component dùng. Hàm viết ở đây chỉ có thể dùng để lấy data ra chứ không thể chỉnh sửa (giống như Getter trong OOP – thể hiện tính bao đóng).

Mutations

Mutation là nơi chúng ta thực hiện các thay đổi state. Các hàm trong mutation thường sẽ không nên chứa logic hay nghiệp vụ business gì, nó chỉ nên có một việc là update state (giống reducer của react redux). Nó chạy đồng bộ và một hàm bên trong một mutation được gọi là một Commit.

Actions

Action sẽ thường chứa logic liên quan đến nghiệp vụ business nó không trực tiếp thay đổi state. Nếu muốn thay đổi state chúng ta dùng một Commit đã được định nghĩa tại Mutations. Lí do là bởi vì Actions thường được chạy bất đồng bộ (Code vẫn chạy khi mà actions chưa hoàn thành) và như vậy khi nó hoàn thành thì chúng ta mới nên Commit để change data. Cách hoạt động của actions khá giống với phần middleware saga hay epic của redux saga và redux observable của React.

3. Cấu trúc thư mục store Vuex sao cho hiệu quả

Khi bắt đầu setup Vuex trong project chúng ta sẽ thường có các thành phần ban đầu là src/store, bên trong store sẽ có file index.js để khai báo store:

thư mục store khi mới tạo

Và nội dung ban đầu trong file index.js

src/index.js

Nhưng chúng ta sẽ không viết toàn bộ code trong file index này mà sẽ tách ra thành các module con cho dễ quản lý, mỗi module sẽ có các state, getters, mutations, actions riêng.

Ví dụ về một module products trong store:

cấu trúc file trong module products

Nội dung bên trong các file mình sẽ trình bày khi vào phần ví dụ cụ thể. Lúc này file index.js tại store/index.js sẽ như sau:

src/index.js

Vì mình không sử dụng gì đến các root state hay root getters,... nên mình comment đoạn code bên trên lại chỉ dùng đến modules. Tương tự chúng ta có thể khai báo thêm các module khác bên trong object modules như cart, user,...

4. Vuex hoạt động như nào ?

Dưới đây là sơ đồ minh hoạ cho data flow của Vuex.

Data flow vuex

Theo như sơ đồ trên khi người dùng thực hiện một hành động trên view ví dụ như click vào button, hàm xử lý trong button này sẽ dispatch một action đã khai báo trong phần actions trong store, action này sẽ thực hiện hàm bất đồng bộ nào đó như call api. Sau khi thực hiện xong trả về data từ response, lúc này chúng ta sẽ dùng commit để gọi đến tên mutation đã khai báo trong mutations với đầu vào là tên mutation và payload (nếu có). Khi mutation này được gọi đến nó sẽ thực hiện update state được gọi trong phần thân hàm của nó bằng payload được commit từ action. Sau khi state đã được update sẽ render lại trên view và hiển thị trạng thái trên giao diện mới sau khi thay đổi.

5. Ví dụ về sử dụng Vuex

Mình sẽ làm một ví dụ nhỏ về lấy danh sách các sản phẩm từ api thông qua axios và vuex.

Trước khi bắt đầu chúng ta nên tạo 1 thư mục quản lý các request api trong app và 1 file cấu hình cho axios dùng chung cho các request.

Thư mục apis khai báo các request api và utils/request.js chứa các cấu hình cho axios.
request.js

Mình sẽ giải thích nội dung bên trong file request.js như sau:

  • Đầu tiên chúng ta sẽ khởi tạo 1 axios instance, trong đó:

    • baseURL là api url, ví dụ "https://abc.xyz/api/v1". Khi đã khai báo baseURL mỗi khi thực hiện các request chúng ta sẽ không cần phải viết cả 1 đoạn url + endpoint dài nữa mà chỉ cần điền vào router endpoint cần gọi tới, ví dụ "/products" thay vì "https://abc.xyz/api/v1/products".
    • timeout là khoảng thời gian cho phép thực hiện 1 request, khi request vượt quá khoảng thời gian này sẽ tự ngắt và đóng kết nối.
  • service.interceptors.request.use(): Đây là hàm thiết lập việc gửi request tới server. Ví dụ như header request của chúng ta cần phải gửi kèm token để xác thực với server, chúng ta không thể cứ mỗi lần thực hiện 1 request lại phải lấy token đã lưu ở đâu đó ra rồi lại config lại vào header. Thay vào đó, khi user đã login, trong state có token, chúng ta có thể lưu lại bằng vuex persistedstate, localStorage. Ở đây mình config Authorization header, nếu trong state có token thì sẽ truyền token vào header, từ đó các request nào cần Authorization đều có token.

  • service.interceptors.response.use(): Đây là hàm sẽ thiết lập response trả về, trong hàm này có 2 phần là respone và error.

    • Trong response mình sẽ xử lý khi thực hiện khi có response trả về nhưng trạng thái không thành công sẽ xử lý gì đó, ví dụ như các code 400, 418,...Còn nếu có respone trả về thành công (code 200) thì return về data.
    • Trong error mình sẽ xử lý khi request thất bại, ví dụ như khi có error.response.status = 401 tức là lỗi về authorization, giả sử như phiên đăng nhập hết hạn thì mình có thể gọi 1 hàm dispatch vào action trong store để reset token bằng refreshToken hoặc logout luôn.

Mọi người có thể tham khảo cách cấu hình instace cho axios tại "https://www.npmjs.com/package/axios"

Trong file products.api.js mình sẽ làm mẫu như sau:

products.api.js

service là instance axios đã cấu hình trong file request, url điền vào tên router api hoặc api url khác, method thực hiện (get, post, put, patch, delete), options param hoặc body tuỳ vào method thực hiện.

Thực hiện lấy danh sách sản phẩm bằng Vuex

Đến đây mình sẽ viết nội dung cho các file trong store đã tạo ở trên.

  • state.js
    Khởi tạo state ban đầu cho product list:

carbon--2--1

  • types.js
    Trong file này sẽ khai báo các constant mutation name, thường sẽ viết in hoa bắt đầu bằng từ SET và nối với nhau bởi dấu _ (giống như khai báo action type của redux).

carbon--3-

  • mutations.js
    Trong file này sẽ có các hàm update lại state 1 cách đồng bộ với tên hàm là mutation name đã khai báo trong types.js, tham số thứ nhất là state của module hiện tại, tham số thứ 2 là payload trả về từ action có thể đặt tên là payload, list,etc...

carbon--4-

Trong phần thân hàm này sẽ trỏ đến state muốn thay đổi và update lại nó bằng payload.

  • actions.js
    Đây là nơi viết các hàm bất đồng bộ, 1 hàm trong actions có 2 tham số, tham số thứ nhất là context hoặc {commit}, nếu dùng context chúng ta có thể truy cập được vào rootState, rootGetters, state, commit, dispatch, tham số thứ 2 là param tuỳ chọn có thể là id, page, limit,...

carbon--5-

Vì axios và request api mình đã cấu hình nên chỉ việc lấy ra và dùng thôi. Mình thường sử dụng Promise để trả về response cho component có thể lấy ra để làm gì đó. Trong then() khi get list thành công sẽ sử dụng commit với 2 tham số, tham số thứ nhất là tên mutation SET_PRODUCT để yêu cầu mutation update lại products trong state, tham số thứ 2 là result trả về từ api, lúc này products trong state sẽ được update bằng result, trong catch() thất bại thì reject không commit gì cả.

  • getters.js
    Đây là tuỳ chọn nếu mình muốn lọc products, giả sử mình muốn lấy các sản phẩm có giá lớn hơn 20000.

carbon--8-

Cuối cùng trong file index tại products/index.js mình sẽ export module products.

carbon--7-

Điều chú ý ở đây là namespaced: true giúp chúng ta truy cập vào các thành phần trong mỗi module theo tên module của chúng, vừa có gợi ý code mà lại dễ quản lý. Không nên để namespaced: true ở root.

Sau khi đã thiết lập module products trong store xong chúng ta sẽ thực hiện dispatch action get api từ view. Mình sẽ thực hiện luôn trong file App.vue.

App.vue

Trong methods mình có hàm getProducts(), trong hàm này mình trỏ đến store để dispatch action getListProduct đã viết trong products/actions.js, thường hàm dispatch sẽ có 2 tham số, tham số thứ nhất là 'tên module/tên action', tham số thứ 2 là option param nếu có. Vì action getListProduct trả về 1 Promise nên mình có thể sử dụng then(), catch() ở đây. Promise này có resolve ra response nên mình sẽ lấy res.message từ response để show ra alert message.

Mình không sử dụng commit hay mapMutation trực tiếp trong việc call api tại component vì mình đã commit data trong action sau khi request thành công rồi. Mình chỉ sử dụng "this.$commit('tên mutation', option param)" khi muốn update 1 state hiện tại nào đó 1 cách đồng bộ, ví dụ như khi phân trang page hiện tại là 2 và mình muốn sau khi rời đi sẽ reset lại page bằng 1 thì mình có thể dùng hook beforeRouteLeave hoặc beforeDestroy để gọi commit set lại page bằng 1.

Lấy state ra dùng trong component sao cho hiệu quả?

Cách hiệu quả nhất để lấy 1 hay nhiều state ra dùng trong component vue đó là dùng helper mapState của Vuex. Mình sẽ đặt tên cho list product trong store là list, không bắt buộc phải đặt tên trùng với tên state, sau đó mình dùng arrow function lấy state trỏ đến module products, rồi trỏ đến state products trong module đó.

Sau khi đã có list mình dùng v-for để đổ ra list product vào table, button get list mình sẽ gọi hàm getProducts(), có thể gọi hàm getProducts() trong mounted để khi tải trang xong sẽ call api luôn.

Có một cách nữa để lấy state ra đó là dùng getters, nhưng cách này không khuyến khích vì nó mất thời gian lọc dữ liệu, và khi có nhiều getters sẽ làm cho module phình to. Nếu chỉ lấy state ra dùng mà không cần điều kiện gì cả thì không nên dùng getters.

6. Các helpers trong vuex

Tương ứng với state, actions, mutations, getters Vuex cung cấp 4 helper giúp map các state, actions, mutations, getters để dùng trong component:

  1. mapState
    Khi 1 component cần sử dụng nhiều thuộc tính state hoặc getters, việc khai báo sẽ lặp đi lặp lại và dài dòng. Để giải quyết vấn đề này, Vuex cung cấp mapState để tạo các hàm computed nhóm các state lại giúp chúng ta lấy state ra dễ dàng.
  2. mapGetters
    Cũng giống mapState, mapGetters ánh xạ các getters trong store tới computed.
  3. mapMutation
    mapMutation map các hàm mutation thay cho nhiều hàm this.$store.commit('tên mutation').
  4. mapAction
    mapAction map các hàm dispatch action thay cho nhiều hàm this.$store.dispatch('tên action').

Trong các helpers trên mình chỉ hay dùng mapState và mapGetters.

Tài liệu tham khảo

What is Vuex? | Vuex
Centralized State Management for Vue.js

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.