How Vue 3 Computed Properties Work with Ref Objects in `<script setup>`: Mechanisms and Use Cases
In Vue 3's <script setup> syntax, the computed property creates a lazy, cached derived state that automatically tracks reactive dependencies from ref objects through a three-layer architecture involving compiler auto-imports, runtime SSR-aware wrappers, and core reactivity effects.
In the vuejs/core repository, the <script setup> block serves as compile-time sugar that streamlines the Composition API by automatically exposing top-level variables to the template. When working with Vue 3 computed properties alongside ref objects, understanding the precise mechanism—from compiler injection to reactive dependency tracking—enables you to optimize performance and avoid common reactivity pitfalls.
The Internal Mechanism of computed with ref Objects
Compiler-Level Auto-Import and Registration
The Single-File Component (SFC) compiler in packages/compiler-sfc automatically detects usage of computed within <script setup> blocks. According to the source code, the compiler treats computed as a macro-type import, injecting the necessary dependency resolution without requiring explicit manual imports in many configurations. This registration ensures that any top-level computed declaration becomes part of the component's public instance and template context.
Runtime Wrapper in apiComputed.ts
The actual computed function exported for component usage resides in packages/runtime-core/src/apiComputed.ts. This wrapper extends the base reactivity implementation with Vue-specific behaviors:
- SSR Awareness: The wrapper accepts
isInSSRComponentSetupas a third argument, allowing the reactivity layer to suppress browser-only side effects during server-side rendering. - Recursive Dependency Warnings: In development mode (
__DEV__), ifwarnRecursiveComputedis enabled in the app configuration, the wrapper sets a_warnRecursiveflag on theComputedRefImplinstance to detect circular dependencies.
// packages/runtime-core/src/apiComputed.ts
export const computed: typeof _computed = (getterOrOptions, debugOptions?) => {
const c = _computed(getterOrOptions, debugOptions, isInSSRComponentSetup)
if (__DEV__) {
const i = getCurrentInstance()
if (i && i.appContext.config.warnRecursiveComputed) {
(c as unknown as ComputedRefImpl<any>)._warnRecursive = true
}
}
return c as any
}
Core Reactivity Implementation
The underlying logic executes in packages/reactivity/src/computed.ts, where the ComputedRefImpl class manages lazy evaluation and caching. When you pass a getter function accessing ref values (e.g., count.value), the implementation:
- Creates a
ReactiveEffectthat tracks all dependencies accessed during getter execution. - Maintains a
dirtyflag to cache results until dependencies mutate. - Automatically triggers updates through the
trackandtriggersystem when underlying refs change.
// packages/reactivity/src/computed.ts (simplified)
export function computed(getter) {
let dirty = true
let value
const effect = new ReactiveEffect(getter, () => {
if (!dirty) {
dirty = true
trigger(computedRef, 'set')
}
})
const computedRef = {
get value() {
if (dirty) {
dirty = false
value = effect.run()
}
track(computedRef, 'get')
return value
}
} as ComputedRef
return computedRef
}
Template Exposure and Automatic Unwrapping
Because <script setup> exposes top-level variables automatically, the compiler adds computed declarations to the component's render context. When the template accesses {{ double }} (where double is a computed property based on a ref), Vue automatically unwraps the ComputedRef without requiring .value syntax in the template.
Common Use Cases for Vue 3 Computed Properties with Ref Objects
Derived Values from Multiple Refs
Use computed to combine multiple ref objects into a single derived state. The calculation only re-executes when specific dependencies change, not on every render.
Expensive Calculations
Cache heavy computational logic—such as array reductions or object transformations—that depends on reactive state. The getter runs only when the underlying ref values mutate, preventing performance bottlenecks during component re-renders.
Read-Only Derived State
computed returns a read-only ComputedRef, enforcing immutability. This pattern protects derived state from accidental mutations while allowing the template and script to react to changes in source refs.
SSR-Safe Computations
The apiComputed.ts wrapper automatically handles server-side rendering constraints, ensuring computed values hydrate consistently between server and client without executing browser-specific side effects during SSR.
Practical Implementation Examples
Simple derived value combining refs:
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('Jane')
const lastName = ref('Doe')
// Updates only when firstName or lastName changes
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>
<template>
<p>{{ fullName }}</p>
<input v-model="firstName" placeholder="First name"/>
<input v-model="lastName" placeholder="Last name"/>
</template>
Expensive calculation with caching:
<script setup lang="ts">
import { ref, computed } from 'vue'
const numbers = ref([1, 2, 3, 4, 5])
// Recomputes only when numbers array reference changes
const sum = computed(() => {
console.log('recomputing sum')
return numbers.value.reduce((a, b) => a + b, 0)
})
</script>
<template>
<p>Sum: {{ sum }}</p>
<button @click="numbers.push(numbers.length + 1)">Add number</button>
</template>
SSR-aware boolean computation:
<script setup lang="ts">
import { ref, computed } from 'vue'
const counter = ref(0)
const isEven = computed(() => counter.value % 2 === 0)
</script>
<template>
<p>{{ isEven ? 'Even' : 'Odd' }}</p>
<button @click="counter++">Increment</button>
</template>
Key Source Files in vuejs/core
Understanding the computed implementation requires examining these specific files:
packages/runtime-core/src/apiComputed.ts: Vue-specific wrapper providing SSR context and development warnings for recursive dependencies.packages/reactivity/src/computed.ts: Core implementation containingComputedRefImpl, lazy effect scheduling, and dependency tracking.packages/compiler-sfc/src/script/analyzeScriptBindings.ts: Detectscomputeddeclarations to expose them to the template scope during compilation.packages/compiler-sfc/src/script/resolveType.ts: Handles TypeScript type resolution forcomputedreturn types in<script setup>.
Summary
computedin<script setup>creates a lazy, cached derived state that tracksrefdependencies automatically.- The implementation flows through three layers: compiler auto-import, runtime SSR wrapper (
apiComputed.ts), and core reactivity engine (computed.ts). - Caching occurs via a dirty-check mechanism in
ComputedRefImpl, preventing unnecessary recalculation until dependencies mutate. - Common use cases include deriving values from multiple refs, expensive calculations, enforcing read-only state, and maintaining SSR consistency.
- Top-level
computeddeclarations in<script setup>are automatically exposed to templates without manual return statements.
Frequently Asked Questions
How does computed differ from a regular method in the template?
A regular method executes on every component render, regardless of whether dependencies changed. In contrast, computed caches the result and only re-evaluates when reactive dependencies tracked within its getter function mutate. According to the vuejs/core source, this optimization occurs because ComputedRefImpl maintains a dirty flag that gates execution until the effect scheduler detects a change.
Can I directly assign a value to a computed property?
No. When created with a getter function, computed returns a read-only ComputedRef. Attempting to set .value on a read-only computed triggers a warning in development mode. If you need writable computed properties, you must provide both getter and setter functions to the computed call.
Why isn't my computed property updating when the ref changes?
Ensure you are accessing the .value property of the ref inside the computed getter. The reactivity system only tracks dependencies that are explicitly accessed during the getter's execution. If you pass the ref object itself without unwrapping it, the dependency tracking fails because the object reference remains stable while its internal value changes.
Is computed automatically SSR-safe in Vue 3?
Yes. The wrapper in packages/runtime-core/src/apiComputed.ts automatically passes isInSSRComponentSetup to the reactivity core. This flag ensures that computed effects avoid browser-specific side effects during server-side rendering, maintaining consistent hydration behavior without requiring manual environment checks in your component code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →