跳至主要內容
Skip to content

Vue3 Ref、Reactive、Computed 類型

響應式是 Vue 的核心。本篇將深入介紹 Ref、Reactive、Computed 的類型標註細節。


一、 Ref 類型詳解

1.1 基本類型

typescript
import { ref, type Ref } from 'vue';

// 自動推論
const count = ref(0); // Ref<number>
const name = ref('John'); // Ref<string>
const active = ref(true); // Ref<boolean>

// 明確標註
const age = ref<number>(25);
const title = ref<string>('Hello');

1.2 允許 null

typescript
interface User {
  id: number;
  name: string;
}

// 初始為 null
const user = ref<User | null>(null);

// 之後賦值
user.value = { id: 1, name: 'John' };

// 存取需注意 null
if (user.value) {
  console.log(user.value.name);
}

// 或使用可選鏈
console.log(user.value?.name);

1.3 陣列類型

typescript
interface Todo {
  id: number;
  text: string;
  done: boolean;
}

// 空陣列需明確標註
const todos = ref<Todo[]>([]);

// 有初始值可推論
const numbers = ref([1, 2, 3]); // Ref<number[]>

// 添加元素
todos.value.push({ id: 1, text: 'Learn TS', done: false });

1.4 Ref 類型

typescript
import { ref, type Ref } from 'vue';

// 使用 Ref 類型
function useCounter(): Ref<number> {
  return ref(0);
}

// 解包類型
import { type UnwrapRef } from 'vue';
type Unwrapped = UnwrapRef<Ref<number>>; // number

二、 Reactive 類型詳解

2.1 基本用法

typescript
import { reactive } from 'vue';

// 自動推論
const state = reactive({
  count: 0,
  name: 'John',
});
// { count: number, name: string }

// 明確標註
interface State {
  count: number;
  name: string;
  user: User | null;
}

const state2 = reactive<State>({
  count: 0,
  name: 'John',
  user: null,
});

2.2 避免的寫法

typescript
// 不要用 ref 包裝 reactive
const state = ref(reactive({ count: 0 })); // 不必要

// 不要解構 reactive
const { count } = reactive({ count: 0 });
// count 失去響應性!

// 正確做法:使用 toRefs
import { reactive, toRefs } from 'vue';

const state = reactive({ count: 0, name: 'John' });
const { count, name } = toRefs(state);
// count 和 name 是 Ref

2.3 巢狀物件

typescript
interface Address {
  city: string;
  country: string;
}

interface User {
  name: string;
  address: Address;
}

const user = reactive<User>({
  name: 'John',
  address: {
    city: 'Taipei',
    country: 'Taiwan',
  },
});

// 巢狀屬性也是響應式
user.address.city = 'Kaohsiung';

三、 Computed 類型詳解

3.1 唯讀 computed

typescript
import { ref, computed, type ComputedRef } from 'vue';

const count = ref(0);

// 自動推論
const double = computed(() => count.value * 2);
// ComputedRef<number>

// 明確標註
const formatted = computed<string>(() => {
  return `Count: ${count.value}`;
});

// 使用 ComputedRef 類型
const result: ComputedRef<number> = computed(() => count.value * 2);

3.2 可寫入 computed

typescript
import { ref, computed, type WritableComputedRef } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

const fullName: WritableComputedRef<string> = computed({
  get(): string {
    return `${firstName.value} ${lastName.value}`;
  },
  set(value: string) {
    const parts = value.split(' ');
    firstName.value = parts[0] || '';
    lastName.value = parts[1] || '';
  },
});

// 讀取
console.log(fullName.value);

// 寫入
fullName.value = 'Jane Smith';

3.3 依賴其他 computed

typescript
const count = ref(0);
const double = computed(() => count.value * 2);
const quadruple = computed(() => double.value * 2);

// 類型自動推論
// double: ComputedRef<number>
// quadruple: ComputedRef<number>

四、 toRef 和 toRefs

4.1 toRef

typescript
import { reactive, toRef } from 'vue';

const state = reactive({
  count: 0,
  name: 'John',
});

// 將單個屬性轉成 ref
const countRef = toRef(state, 'count');
// Ref<number>

// 修改會同步
countRef.value++;
console.log(state.count); // 1

4.2 toRefs

typescript
import { reactive, toRefs } from 'vue';

interface State {
  count: number;
  name: string;
}

const state = reactive<State>({
  count: 0,
  name: 'John',
});

// 將所有屬性轉成 ref
const refs = toRefs(state);
// { count: Ref<number>, name: Ref<string> }

// 解構後保持響應性
const { count, name } = toRefs(state);

4.3 組合函式回傳

typescript
import { reactive, toRefs } from 'vue';

function useUser() {
  const state = reactive({
    name: 'John',
    age: 30,
  });

  // 回傳 toRefs 以保持響應性
  return toRefs(state);
}

// 使用
const { name, age } = useUser();
// name: Ref<string>, age: Ref<number>

五、 shallowRef 和 shallowReactive

5.1 shallowRef

typescript
import { shallowRef, type ShallowRef } from 'vue';

interface DeepObject {
  nested: { value: number };
}

const data: ShallowRef<DeepObject> = shallowRef({
  nested: { value: 0 },
});

// 修改 .value 會觸發更新
data.value = { nested: { value: 1 } };

// 修改深層屬性不會觸發
data.value.nested.value = 2; // 不會更新視圖

// 手動觸發
import { triggerRef } from 'vue';
triggerRef(data);

5.2 shallowReactive

typescript
import { shallowReactive } from 'vue';

const state = shallowReactive({
  count: 0,
  nested: { value: 0 },
});

// 第一層是響應式
state.count = 1; // 觸發更新

// 巢狀不是響應式
state.nested.value = 1; // 不觸發更新

六、 readonly

6.1 基本用法

typescript
import { reactive, readonly } from 'vue';

const original = reactive({ count: 0 });
const readonlyCopy = readonly(original);

// 只能讀取
console.log(readonlyCopy.count);

// 嘗試修改會報錯(開發模式)
// readonlyCopy.count = 1 // 警告!

// 原始物件修改會反映
original.count = 1;
console.log(readonlyCopy.count); // 1

6.2 類型

typescript
import { readonly, type DeepReadonly } from 'vue';

interface State {
  count: number;
  nested: { value: number };
}

const state = reactive<State>({
  count: 0,
  nested: { value: 0 },
});

const readonlyState: DeepReadonly<State> = readonly(state);

七、 實用範例

7.1 表單狀態

typescript
import { reactive, computed } from 'vue';

interface FormState {
  username: string;
  email: string;
  password: string;
}

interface FormErrors {
  username?: string;
  email?: string;
  password?: string;
}

const form = reactive<FormState>({
  username: '',
  email: '',
  password: '',
});

const errors = reactive<FormErrors>({});

const isValid = computed(() => {
  return !errors.username && !errors.email && !errors.password;
});

function validate() {
  errors.username = form.username.length < 3 ? '至少 3 個字元' : undefined;
  errors.email = !form.email.includes('@') ? '無效的 email' : undefined;
  errors.password = form.password.length < 6 ? '至少 6 個字元' : undefined;
}

7.2 API 狀態

typescript
import { ref, computed } from 'vue';

interface User {
  id: number;
  name: string;
}

const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<Error | null>(null);

const hasUsers = computed(() => users.value.length > 0);
const firstUser = computed(() => users.value[0] ?? null);

async function fetchUsers() {
  loading.value = true;
  error.value = null;

  try {
    const response = await fetch('/api/users');
    users.value = await response.json();
  } catch (e) {
    error.value = e as Error;
  } finally {
    loading.value = false;
  }
}

總結

API用途類型
ref<T>()單值響應式Ref<T>
reactive<T>()物件響應式T
computed<T>()計算屬性ComputedRef<T>
toRef()單屬性轉 RefRef<T>
toRefs()全屬性轉 Refs{ [K]: Ref<T[K]> }
readonly()唯讀DeepReadonly<T>

> **選擇建議**:

  • 原始值用 ref
  • 物件/陣列用 reactive
  • 解構用 toRefs

進階挑戰

  1. 實作一個 useToggle 組合函式
  2. 建立一個帶有 loading/error 狀態的 useRequest<T>
  3. 實作一個 useDebounceRef<T>

延伸閱讀與資源