本文 AI 產出,尚未審核

Vue3 Composition API(核心)

toRef()、toRefs()、unref()


簡介

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

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


核心概念

1. refreactive 的差別

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

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

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

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

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

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

3. toRef(source, key?)

  • 參數

    • sourcereactive 物件或 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)

  • 參數:可能是 refreactive、或普通值。
  • 回傳:如果傳入的是 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:在模板中直接使用 emailpassword 等變數,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>

關鍵:父層使用 toRefsreactive 轉成「可解構的 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 中使用 unreftoRefs 讓變數直接是 ref,減少 .value 的出現。
ref 直接傳給非 Vue 函式 某些第三方函式期望普通值,傳入 ref 會出錯。 使用 unref 先取得原始值再傳入。
watch 中使用 toRef 產生的 ref watch 的第一個參數是 ref.value,會失去追蹤。 直接把 ref 本身作為第一個參數,或使用 watchEffect

最佳實踐

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

實際應用場景

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

總結

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

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