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 isInSSRComponentSetup as a third argument, allowing the reactivity layer to suppress browser-only side effects during server-side rendering.
  • Recursive Dependency Warnings: In development mode (__DEV__), if warnRecursiveComputed is enabled in the app configuration, the wrapper sets a _warnRecursive flag on the ComputedRefImpl instance 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:

  1. Creates a ReactiveEffect that tracks all dependencies accessed during getter execution.
  2. Maintains a dirty flag to cache results until dependencies mutate.
  3. Automatically triggers updates through the track and trigger system 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:

Summary

  • computed in <script setup> creates a lazy, cached derived state that tracks ref dependencies 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 computed declarations 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →