本文 AI 產出,尚未審核

Vue3 Composition API(核心)

toRef()、toRefs()、unref()


簡介

在 Vue 3 中,Composition API 讓我們可以以函式的方式組織邏輯,提升程式碼的可重用性與可測試性。
當我們在 setup() 內使用 reactive() 產生一個「深層響應式」的物件時,往往需要把其中的某些屬性解構(destructure)或傳遞給其他函式。若直接解構,原本的響應式連結會被斷開,導致 UI 不會更新。

toRef()、toRefs() 與 unref() 正是為了解決 「從 reactive 物件取得單一屬性、保持響應式」 以及 「從 ref 取得原始值」 這兩個常見需求而設計的工具函式。掌握它們的使用方式,能讓你在開發大型 Vue 專案時,避免許多微妙的 bug,並寫出更乾淨、可維護的程式碼。


核心概念

1. ref 與 reactive 的差別

ref(value) reactive(object)
用途 包裝單一值(原始值或物件) 包裝整個物件,深層轉成響應式
取得值方式 myRef.value 直接使用屬性 state.count
常見情境 表單欄位、計數器、簡單布林 複雜資料結構、API 回傳的 JSON

Tip:reactive 內部會自動把每個屬性轉成 ref,但它本身不是 ref,因此不能直接使用 .value。

2. 為什麼需要 toRef()、toRefs()

當我們把 reactive 物件解構成普通變數時:

const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state   // ❌ count、name 失去響應式

此時 count、name 成為普通的 JavaScript 變數,Vue 再也無法追蹤它們的變化。
toRef() 與 toRefs() 正是用來 保持單個屬性的響應式連結,讓解構後仍能自動更新 UI。

3. toRef(source, key?)

  • 參數

    • source:reactive 物件或 ref 包裝的物件。
    • key(可選):要取得的屬性名稱,若省略則回傳整個 source 本身的 ref。
  • 回傳:一個指向 source[key] 的 ref,即使 source 本身變化,這個 ref 仍會保持同步。

const state = reactive({ count: 0 })
const countRef = toRef(state, 'count')   // countRef === ref(0)
countRef.value++                         // state.count 也會跟著變

4. toRefs(source)

  • 參數:reactive 物件或 ref 包裝的物件。
  • 回傳:一個新物件,裡面的每個屬性都是對應 source 中屬性的 ref。

這是將 整個物件 同時「轉成可解構的 refs」的快捷方式。

const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)   // count、name 仍是 ref
count.value++                           // state.count 會同步變化

注意:toRefs() 只會對第一層屬性產生 ref,深層物件仍保持原本的 reactive 行為。

5. unref(value)

  • 參數:可能是 ref、reactive、或普通值。
  • 回傳:如果傳入的是 ref,則回傳其 .value;否則直接回傳原始值。

在模板或計算屬性裡,我們常會寫 someRef.value,但在 JavaScript 中有時希望自動「解包」:

function double(v) {
  return unref(v) * 2
}
const count = ref(5)
console.log(double(count))   // 10
console.log(double(3))       // 6

程式碼範例

以下示範 5 個常見情境,從基礎到實務應用,一步步說明 toRef()、toRefs()、unref() 的使用方式。

範例 1:單屬性保持響應式(toRef)

import { reactive, toRef, watch } from 'vue'

export default {
  setup() {
    const user = reactive({ name: 'Alice', age: 25 })

    // 只想監聽 name,卻不想把整個物件傳給子組件
    const nameRef = toRef(user, 'name')

    // 監聽 name 的變化
    watch(nameRef, (newName) => {
      console.log('使用者名稱變更為', newName)
    })

    // 在其他地方修改
    setTimeout(() => {
      nameRef.value = 'Bob'   // -> console 會印出變更訊息
    }, 1000)

    return { nameRef }
  }
}

重點:nameRef 仍然是 ref,所以子組件若直接接受 nameRef,仍能保持雙向綁定。

範例 2:一次解構多屬性(toRefs)

import { reactive, toRefs } from 'vue'

export default {
  setup() {
    const form = reactive({
      email: '',
      password: '',
      remember: false
    })

    // 直接解構,保持每個欄位都是 ref
    const { email, password, remember } = toRefs(form)

    // 送出表單前的驗證函式
    const submit = () => {
      if (!email.value.includes('@')) {
        alert('Email 格式錯誤')
        return
      }
      console.log('提交資料', {
        email: email.value,
        password: password.value,
        remember: remember.value
      })
    }

    return { email, password, remember, submit }
  }
}

Tip:在模板中直接使用 email、password 等變數,Vue 會自動解包 .value,寫起來更乾淨。

範例 3:將 reactive 物件傳給子組件(toRefs + 解構)

<!-- Parent.vue -->
<template>
  <Child v-bind="userRefs" />
</template>

<script setup>
import { reactive, toRefs } from 'vue'
import Child from './Child.vue'

const user = reactive({ id: 1, username: 'admin', role: 'editor' })
const userRefs = toRefs(user)   // { id: Ref, username: Ref, role: Ref }
</script>
<!-- Child.vue -->
<template>
  <div>
    <p>使用者名稱: {{ username }}</p>
    <p>角色: {{ role }}</p>
    <button @click="changeName">改名為 Guest</button>
  </div>
</template>

<script setup>
defineProps({
  id: Number,
  username: String,
  role: String
})

// 因為父層傳來的是 Ref,直接改 .value 即可同步
function changeName() {
  // `username` 其實是 Ref,Vue 會自動把它解包為 .value
  // 但在 setup 中若要手動改,仍需 `.value`
  username.value = 'Guest'
}
</script>

關鍵:父層使用 toRefs 把 reactive 轉成「可解構的 refs」,子組件接受的仍是 ref,因此雙向綁定不會斷。

範例 4:使用 unref 簡化通用函式

import { ref, unref } from 'vue'

function clamp(value, min, max) {
  const v = unref(value)        // 若是 ref,取 .value;否則直接使用
  const lower = unref(min)
  const upper = unref(max)
  return Math.min(Math.max(v, lower), upper)
}

// 測試
const num = ref(15)
console.log(clamp(num, 0, 10))   // 10
console.log(clamp(5, 0, 10))     // 5

好處:clamp 可以接受 ref 或普通值,使用者不必在呼叫前自行 .value,提升 API 的彈性。

範例 5:與 watchEffect 結合的技巧

import { reactive, toRefs, watchEffect, unref } from 'vue'

export default {
  setup() {
    const settings = reactive({
      darkMode: false,
      fontSize: 14
    })

    const { darkMode, fontSize } = toRefs(settings)

    // 使用 unref 讓 watchEffect 更簡潔
    watchEffect(() => {
      const mode = unref(darkMode) ? 'dark' : 'light'
      const size = unref(fontSize)
      document.body.dataset.theme = mode
      document.body.style.fontSize = `${size}px`
    })

    // 測試變更
    setTimeout(() => {
      darkMode.value = true
      fontSize.value = 18
    }, 2000)

    return { darkMode, fontSize }
  }
}

說明:watchEffect 內部自動追蹤 ref,但若想在外部函式裡取得值,unref 能讓程式碼保持「純值」的感覺。


常見陷阱與最佳實踐

陷阱 說明 解決方式
解構後失去響應式 直接 const { foo } = reactiveObj 會得到普通值。 使用 toRef(reactiveObj, 'foo') 或 toRefs(reactiveObj)。
多層物件仍是 reactive toRefs 只會處理第一層,深層結構仍是 reactive,若再解構會斷連。 若需要深層解構,手動對每層使用 toRef,或考慮使用 shallowRef 包裝。
在模板外忘記 .value 在 setup() 中使用 ref 時,必須寫 myRef.value;在模板裡則會自動解包。 盡量在 setup 中使用 unref 或 toRefs 讓變數直接是 ref,減少 .value 的出現。
將 ref 直接傳給非 Vue 函式 某些第三方函式期望普通值,傳入 ref 會出錯。 使用 unref 先取得原始值再傳入。
在 watch 中使用 toRef 產生的 ref 若 watch 的第一個參數是 ref.value,會失去追蹤。 直接把 ref 本身作為第一個參數,或使用 watchEffect。

最佳實踐

  1. 盡量使用 toRefs 解構:在 setup 中把 reactive 物件一次性轉成可解構的 refs,讓後續的代碼既簡潔又保持響應式。
  2. 在公共函式裡使用 unref:讓函式接受 ref 或普通值,提高 API 的彈性。
  3. 避免深層解構:如果真的需要深層解構,先把深層物件抽成獨立的 reactive 或 ref,再套用 toRef。
  4. 命名慣例:對於 ref,建議在變數名稱後加上 Ref(如 countRef),或使用 toRefs 直接取得同名的 ref(count),讓閱讀者一眼就能分辨。
  5. 使用 TypeScript 時:toRefs 會正確推斷類型,搭配 defineProps、defineEmits 可得到完整的型別提示。

實際應用場景

場景 為什麼需要 toRef / toRefs / unref
表單元件的雙向綁定 父層使用 reactive 管理所有欄位,子元件只需要單一欄位的 ref,使用 toRef 傳遞即可保持同步。
跨組件共享狀態 透過 provide/inject 提供 reactive 物件,子組件使用 toRefs 解構後直接使用,避免手動 .value。
自訂 Hook / Composable 內部使用 reactive 保存狀態,對外回傳 toRefs(state),讓使用者可以自由解構並直接在模板中使用。
第三方函式庫的 API 如 lodash、date-fns 等純 JavaScript 函式只接受普通值,使用 unref 包裝可直接傳入 ref。
動態樣式或主題切換 把主題設定放在 reactive,使用 toRefs 把 darkMode、primaryColor 等屬性導出,watchEffect 結合 unref 即可即時更新 DOM。

總結

  • toRef(source, key):把 reactive(或 ref 包裝的物件)中的單一屬性轉成 ref,保持響應式。
  • toRefs(source):一次把整個 reactive 物件的第一層屬性全部轉成 ref,便於解構與傳遞。
  • unref(value):自動取得 ref 的 .value,若不是 ref 則直接回傳原值,讓函式更具彈性。

在 Vue 3 的 Composition API 中,這三個工具函式是 保持響應式完整性、提升程式碼可讀性與可重用性的關鍵。只要遵循上述最佳實踐,避免常見的解構斷鏈陷阱,就能寫出乾淨、易維護的 Vue 應用程式。祝開發順利,玩得開心! 🚀