本文 AI 產出,尚未審核

Vue3 Composition API(核心)── 回傳 template context


簡介

在 Vue 3 中,Composition API 讓我們可以把組件的邏輯抽離成可重用的函式,而 setup() 則是所有邏輯的入口。setup() 執行完畢後,Vue 會將 返回值(return value) 作為 template context,也就是模板中可以直接使用的變數、函式或計算屬性。

掌握「回傳 template context」的細節,不僅能讓模板保持簡潔、易讀,還能避免常見的 reactivity 失效或命名衝突問題。因此,這篇文章會從概念說明、實作範例、常見陷阱與最佳實踐,一直到實務應用,完整介紹如何在 Composition API 中正確地把資料提供給模板使用。


核心概念

1. setup() 的返回值會自動映射到模板

export default {
  setup() {
    const count = ref(0)          // reactive 狀態
    const inc = () => count.value++   // 方法

    // 只要把需要在模板中使用的變數/函式回傳,
    // Vue 會把它們掛到模板的作用域上
    return { count, inc }
  }
}

在上述範例中,{{ count }} 會自動解包 ref,顯示 count.value 的值;@click="inc" 也能直接呼叫 inc

重點:返回的物件會被 reactive 包裝(即使是普通物件),因此在模板中使用時會自動保持響應式。


2. refreactivetoRefs 的差異

方法 說明 在模板中的使用方式
ref() 包裝單一值或物件,須以 .value 讀寫 自動解包({{ myRef }})或在 JS 中使用 .value
reactive() 深度響應式的物件 直接使用屬性({{ state.name }}
toRefs() reactive 物件的每個屬性轉成 ref,避免解構時失去響應式 ref,在解構後仍保有響應式

範例 1:使用 reactive + toRefs

export default {
  setup() {
    const state = reactive({
      name: 'Vue',
      age: 3
    })

    // 若直接解構會失去響應式,必須使用 toRefs
    const { name, age } = toRefs(state)

    const increaseAge = () => age.value++

    return { name, age, increaseAge }
  }
}
<!-- template -->
<div>
  <p>名稱:{{ name }}</p>
  <p>年齡:{{ age }}</p>
  <button @click="increaseAge">長大一年</button>
</div>

3. computed 也是回傳給模板的常見類型

export default {
  setup() {
    const firstName = ref('John')
    const lastName  = ref('Doe')

    const fullName = computed(() => `${firstName.value} ${lastName.value}`)

    return { firstName, lastName, fullName }
  }
}
<p>全名:{{ fullName }}</p>

computed 會在依賴變更時自動重新計算,且在模板中同樣會自動解包。


4. defineExpose<script setup>)讓子組件顯式暴露 context

<script setup> 中,所有在 setup() 內定義的變數預設會曝光給模板,但若想讓父組件透過 ref 取得子組件的內部方法,需使用 defineExpose

<!-- Child.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
const reset = () => (count.value = 0)

defineExpose({ reset })   // 暴露給父組件
</script>

<template>
  <p>計數:{{ count }}</p>
</template>

父組件:

<template>
  <Child ref="childRef" />
  <button @click="childRef?.reset()">重設子組件計數</button>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)
</script>

5. script setup 的隱式返回

使用 <script setup> 時,不需要手動 return,所有在頂層聲明的 refreactivecomputed、函式,都會自動成為模板的 context:

<script setup>
import { ref, computed } from 'vue'

const price = ref(100)
const qty   = ref(2)

const total = computed(() => price.value * qty.value)
</script>

<template>
  <p>總價:{{ total }}</p>
</template>

這讓程式碼更簡潔,也減少忘記 return 的錯誤。


程式碼範例(實用示例)

範例 1:表單雙向綁定 + 表單驗證

<script setup>
import { ref, computed } from 'vue'

const form = reactive({
  email: '',
  password: ''
})

// 簡易驗證規則
const emailError = computed(() =>
  form.email.includes('@') ? '' : '請輸入有效的 Email'
)
const passwordError = computed(() =>
  form.password.length >= 6 ? '' : '密碼至少 6 個字元'
)

const canSubmit = computed(() => !emailError.value && !passwordError.value)

function submitForm() {
  if (canSubmit.value) {
    alert(`送出資料:${JSON.stringify(form)}`)
  }
}
</script>

<template>
  <form @submit.prevent="submitForm">
    <label>
      Email:
      <input v-model="form.email" type="email" />
    </label>
    <span class="error">{{ emailError }}</span>

    <label>
      Password:
      <input v-model="form.password" type="password" />
    </label>
    <span class="error">{{ passwordError }}</span>

    <button :disabled="!canSubmit">送出</button>
  </form>
</template>

說明:所有的 reactivecomputedfunction 都直接在模板中使用,沒有額外的 return,因為 <script setup> 會自動把它們加入 context。


範例 2:使用 toRefs 讓解構保持響應式

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

    // 正確做法:使用 toRefs
    const { name, age } = toRefs(user)

    const birthday = () => age.value++

    return { name, age, birthday }
  }
}
<div>
  <p>姓名:{{ name }}</p>
  <p>年齡:{{ age }}</p>
  <button @click="birthday">慶祝生日</button>
</div>

陷阱:如果直接寫 const { name, age } = usernameage 會變成普通值,模板不會再更新。


範例 3:子組件暴露方法給父組件(defineExpose

<!-- Counter.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
function inc() { count.value++ }
function reset() { count.value = 0 }

defineExpose({ inc, reset })
</script>

<template>
  <p>計數:{{ count }}</p>
</template>
<!-- Parent.vue -->
<template>
  <Counter ref="counterRef" />
  <button @click="counterRef?.inc()">+1</button>
  <button @click="counterRef?.reset()">重設</button>
</template>

<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'

const counterRef = ref(null)
</script>

關鍵defineExpose 只在 <script setup> 中使用,讓父組件可以安全地呼叫子組件內部的函式。


範例 4:動態列表與 key 的最佳實踐

export default {
  setup() {
    const items = ref([
      { id: 1, text: '第一項' },
      { id: 2, text: '第二項' }
    ])

    const addItem = () => {
      const nextId = items.value.length + 1
      items.value.push({ id: nextId, text: `第 ${nextId} 項` })
    }

    const removeItem = (id) => {
      items.value = items.value.filter(item => item.id !== id)
    }

    return { items, addItem, removeItem }
  }
}
<ul>
  <li v-for="item in items" :key="item.id">
    {{ item.text }}
    <button @click="removeItem(item.id)">刪除</button>
  </li>
</ul>
<button @click="addItem">新增項目</button>

說明itemsref 包裹的陣列,返回後直接在模板使用 v-for,而 :key 必須使用唯一的 id,才能讓 Vue 正確追蹤項目。


範例 5:使用 watch 監聽返回的變數

import { ref, watch } from 'vue'

export default {
  setup() {
    const query = ref('')
    const results = ref([])

    // 監聽 query 變化,模擬搜尋 API
    watch(query, async (newVal) => {
      if (newVal) {
        // 假裝呼叫 API
        results.value = await fakeApiSearch(newVal)
      } else {
        results.value = []
      }
    })

    return { query, results }
  }
}

async function fakeApiSearch(q) {
  // 模擬延遲
  return new Promise(resolve => {
    setTimeout(() => resolve([`${q} 結果 1`, `${q} 結果 2`]), 500)
  })
}
<input v-model="query" placeholder="輸入關鍵字" />
<ul>
  <li v-for="r in results" :key="r">{{ r }}</li>
</ul>

技巧watch 必須在 setup() 內部建立,且監聽的目標必須是返回給模板的 refcomputed,才能保證資料流的完整性。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方案 / 最佳實踐
忘記回傳需要的變數 模板報錯 Property or method "xxx" is not defined 確認 setup() 最後 return 的物件包含所有模板需要的項目,或使用 <script setup> 免除手動 return
直接解構 reactive 物件 失去響應式,畫面不會更新 使用 toRefs()readonly(),或直接在模板中使用 state.prop
在模板中直接使用 .value 失去自動解包的便利,且在 <script setup> 中會造成不必要的冗餘 讓 Vue 自動解包,除非在 JavaScript 中需要讀寫 .value
返回過大的物件 每次渲染都會重新建立,影響效能 只回傳「模板需要」的最小集合,其他可放在局部變數或 useXxx composable 中。
命名衝突(props vs return) props 會被覆寫,導致預期外行為 若需要同名,使用 ...props 展開後自行重新命名,或避免同名。
<script setup> 中使用 defineExpose 時忘記導入 defineExpose 未被辨識,編譯錯誤 確保 import { defineExpose } from 'vue'(Vue 3.3+)或直接使用全域 defineExpose(已自動注入)。

最佳實踐清單

  1. 最小化返回:只回傳模板真正需要的狀態與方法。
  2. 使用 toRefs:在需要解構 reactive 時,務必使用 toRefs 保持響應式。
  3. 保持命名一致:盡量避免在 propssetup 返回值與 data 中使用相同名稱。
  4. 利用 script setup:簡化語法、降低忘記 return 的機會。
  5. 適時使用 defineExpose:在需要父組件直接呼叫子組件方法時才使用,避免過度暴露內部實作。
  6. 測試 reactivity:開發時使用 Vue Devtools 檢查返回的變數是否為 ref/reactive,確保更新能即時反映。

實際應用場景

  1. 表單與驗證

    • 多欄位表單的狀態、錯誤訊息與提交函式全部放在 setup(),返回給模板,讓 UI 與驗證邏輯清晰分離。
  2. 資料表格與分頁

    • setup() 內部管理 currentPagepageSizesortedData,返回給模板後配合 watchcomputed 產生即時的分頁結果。
  3. 彈窗/對話框的開關狀態

    • 使用 ref 控制 isOpen,以及 open()close() 方法返回,讓父子組件透過 v-modelexpose 輕鬆控制。
  4. 跨組件共享邏輯(Composable)

    • 把共用的資料抓取、緩存、錯誤處理封裝成 useFetch(),在各個組件的 setup() 中呼叫,僅返回需要渲染的 dataerrorloading
  5. 動畫與過渡控制

    • ref 保存動畫狀態(如 isAnimating),返回後在模板中使用 v-ifv-showtransition 組件,確保動畫與資料同步。

總結

  • 回傳 template context 是 Vue 3 Composition API 的核心機制之一。只要在 setup() 中正確返回需要的 refreactivecomputed 與方法,模板就能直接使用,且保持完整的響應式特性。
  • 使用 toRefsdefineExpose、以及 <script setup>,能讓程式碼更安全、易讀與維護。
  • 透過本篇提供的概念說明與實作範例,你可以在表單、列表、跨組件共享、以及父子組件互動等多種情境中,靈活運用「回傳 template context」的技巧。
  • 最後,別忘了 最小化返回內容避免解構失效,以及 善用 Vue Devtools 觀察 reactivity,這樣才能寫出既高效又易除錯的 Vue 3 應用程式。

祝開發順利,玩得開心! 🎉