Vue 3 Composition API: Giải pháp tối ưu cho việc tổ chức Logic Component

Giới thiệu về Composition API

Composition API là một phần mở rộng mới mẻ được giới thiệu trong phiên bản Vue 3.x, đánh dấu sự chuyển dịch khỏi mô hình Options API truyền thống. Trong phương pháp cũ, mã nguồn liên quan đến nghiệp vụ thường bị phân tán rải rác vào các thuộc tính như data, methods, hoặc computed. Điều này hoạt động ổn đối với các dự án nhỏ, nhưng gây khó khăn đáng kể trong việc duy trì và tái sử dụng logic tại các dự án phức tạp quy mô lớn.

Mục tiêu chính của Composition API là gom nhóm logic lại thành các khối chức năng nhất quán (logic grouping), cải thiện khả năng quản lý trạng thái và tăng cường tính linh hoạt cho lập trình viên khi xây dựng các component phức tạp.

Các Hook Cơ Bản của Composition API

Hệ sinh thái của Composition API cung cấp tập hợp các công cụ chính sau đây:

  • setup(): Điểm khởi đầu bắt buộc của lifecycle.
  • ref()reactive(): Quản trị phản ứng (reactivity).
  • computed(): Tính toán biến đổi từ dữ liệu gốc.
  • watch() / watchEffect()
  • toRefs() / toRef()
  • lifecycle hooks: Các vòng đời như onMounted, onUnmounted, v.v.

1. Hàm setup()

Đây là hook mặc định để truy cập vào Context của component trong Vue 3. Nó đóng vai trò là điểm nhập duy nhất để thiết lập dữ liệu và methods.

Tuần tự thực thi

Hàm setup sẽ được gọi trước cả生命周期 createdbeforeCreate. Tại thời điểm này, instance của component chưa được tạo ra hoàn toàn, do đó bạn sẽ không thể truy cập trực tiếp this. Mọi biến cục bộ hay method chỉ có thể truy cập thông qua đối tượng trả về từ hàm setup hoặc thông qua props đã truyền vào.

Các tham số đầu vào

Hàm nhận hai tham số chính:

  1. props: Chứa tất cả các thuộc tính cha truyền xuống. Lưu ý rằng props luôn ở dạng Reactive, nên tránh sử dụng ES6 destructuring trực tiếp vì nó làm mất tính gắn kết của dữ liệu.
  2. context: Một object chứa thông tin về phạm vi, bao gồm attrs, slots, parent, root và các method như emit.
export default {
  props: ['username'],
  setup(props) {
    console.log(`Tên người dùng là: ${props.username}`);
    
    // Truy xuất context nếu cần
    const emit = () => {}; 
    
    return {};
  }
}

Nếu muốn an toàn khi khai báo biến từ props mà vẫn giữ tính Reactive, hãy sử dụng hàm toRefs:

import { toRefs, ref } from 'vue';

setup(props) {
  // Cấu hình an toàn
  const { username } = toRefs(props);
  
  return {
    username
  }
}

2. Quét dữ liệu phản hồi với ref()

Hàm ref dùng để tạo ra biến phản hồi cho các loại dữ liệu nguyên thủy (String, Number, Boolean, Array...). Kết quả trả về là một object wrapper có thuộc tính .value để lưu trữ giá trị thực.

import { ref } from 'vue';

setup() {
  const countVal = ref(0);
  
  // Access value phải dùng .value
  console.log(countVal.value); // 0
  
  // Update value
  countVal.value++; 
  
  return { countVal };
}

Khi gán một ref mới vào trong một object reactive, hệ thống sẽ phát hiện sự thay thế của reference. Tuy nhiên, giá trị bên trong của ref cũ sẽ không tự động cập nhật trong biến gốc nếu không được gán đúng cách.

const c1 = ref(0);
const state = reactive({ c1 });

c1.value = 9; // Cập nhật đúng cách
console.log(state.c1.value); // 9

Quy tắc vàng:

  • Trong script: Luôn dùng .value để đọc/ghi.
  • Trong Template: Vue tự động unwrap, có thể dùng thẳng tên biến.

3. Tạo trạng thái đối tượng với reactive()

Lệnh reactive phù hợp để quản lý các object hoặc map dữ liệu phức tạp. Khác với ref, biến trả về là một Proxy Object, không cần .value khi truy cập properties.

import { reactive } from 'vue';

setup() {
  const userInfo = reactive({
    fullName: 'Nguyễn Văn A',
    role: 'Admin'
  });
  
  return {
    userInfo
  };
}

4. Destructuring với toRefs()

Khi sử dụng reactive, ta không thể tách biến (destructure) trực tiếp trong phần return vì điều đó sẽ cắt đứt liên kết phản xạ. Để khắc phục, Vue cung cấp toRefs biến đổi từng thuộc tính thành một ref riêng biệt.

import { toRefs, reactive } from 'vue';

setup() {
  const person = reactive({ name: 'Bùi Thị B', age: 25 });
  
  return {
    // Chuyển toàn bộ person thành refs độc lập
    ...toRefs(person) 
  };
}

// Trong template có thể dùng thẳng name, age mà vẫn update được UI
// {{ name }}

5. Thuộc tính tính toán computed()

Hàm computed tạo ra các biến phụ thuộc, giống như computed trong Options API nhưng tuân thủ quy tắc của Composition API. Kết quả trả về luôn là một ref readonly (chỉ đọc).

import { ref, computed } from 'vue';

setup() {
  const basePrice = ref(100000);
  const taxRate = ref(0.1);
  
  const totalCost = computed(() => {
    return basePrice.value + (basePrice.value * taxRate.value);
  });
  
  return {
    totalCost,
    basePrice
  };
}

6. Dữ liệu chỉ đọc readonly()

Dùng readonly để đóng gói một object hoặc ref, ngăn chặn mọi sự chỉnh sửa. Mọi cố gắng thay đổi giá trị sẽ phát sinh cảnh báo trong console giúp debug lỗi vô tình.

import { reactive, readonly } from 'vue';

setup() {
  const config = reactive({ port: 3000 });
  const protectedConfig = readonly(config);
  
  // protectedConfig.port = 4000; // Lỗi runtime
  return { protectedConfig };
}

7. Quan sát sự thay đổi với watch()

Hook watch hoạt động kiểu Lazy (thụ động). Nó chỉ kích hoạt khi dữ liệu nguồn thay đổi, không chạy ngay lúc mount.

import { ref, watch } from 'vue';

setup() {
  const currentKeyword = ref('');
  
  watch(currentKeyword, (newValue, oldValue) => {
    console.log('Từ khóa thay đổi:', newValue, oldValue);
  });
  
  return { currentKeyword };
}

Bạn cũng có thể quan sát nhiều nguồn cùng lúc hoặc quan sát property con của object reactive bằng cách truyền callback getter.

const profile = reactive({ firstName: '', lastName: '' });

watch(
  () => profile.firstName, 
  (val) => alert(val)
);

// Quan sát mảng nhiều dữ liệu
watch(
  [() => profile.firstName, () => profile.lastName], 
  ([fName, lName]) => { /* ... */ }
);

Sử dụng tùy chọn { immediate: true } để buộc hàm chạy ngay lần đầu tiên khi mount.

8. Hiệu ứng tức thì với watchEffect()

Khác với watch, watchEffect tự động thu thập các dependencies nằm trong closure của callback. Nó chạy ngay lập tức (không Lazy) khi component mount.

import { ref, watchEffect } from 'vue';

setup() {
  const activeCount = ref(5);
  
  watchEffect(() => {
    // Tự động theo dõi activeCount.value
    console.log('Số lượng đang hoạt động:', activeCount.value);
  });
  
  return { activeCount };
}

Sự khác biệt chính giữa Watch và WatchEffect:

  • Watch: Bạn phải khai báo rõ nguồn dữ liệu giám sát. Có thể truy cập giá trị cũ và mới.
  • WatchEffect: Tự xác định nguồn phụ thuộc dựa trên lệnh đọc. Chỉ biết giá trị mới hiện tại.

9. Truyền dữ liệu xuyên suốt cây thành phần Provide/Inject

Tránh hiện tượng "prop drilling" (truyền props qua nhiều tầng component), Vue 3 hỗ trợ cơ chế Dependency Injection.

Cấu hình Parent (Cung cấp)

import { provide, ref } from 'vue';

setup() {
  const siteName = ref('TechHub Vietnam');
  
  // Cung cấp cho tất cả child descendants
  provide('appName', siteName);
  
  return {};
}

Cấu hình Child (Nhận giá trị)

import { inject } from 'vue';

setup() {
  // Lấy giá trị cung cấp từ cha xa xôi
  const siteName = inject('appName', 'Default Name');
  
  return { siteName };
}

Cơ chế này đảm bảo tính Reactive, nghĩa là nếu parent thay đổi giá trị ref, tất cả các component con đang subscribe đều được cập nhật tương ứng mà không cần can thiệp thêm.

Thẻ: Vue3 CompositionAPI reactivity JavaScript FrontendDevelopment

Đăng vào ngày 21 tháng 7 lúc 23:07