# How to Use Vue Watch to Detect Changes in Nested Data Objects

> Learn how to use Vue watch to detect changes in nested data objects. Enable the deep option for recursive tracking or specify a numeric depth to limit traversal.

- Repository: [Vue/core](https://github.com/vuejs/core)
- Tags: how-to-guide
- Published: 2026-02-19

---

**Enable the `deep` option in Vue's `watch` API to recursively track mutations at any level of a nested object, or provide a numeric depth value to limit the traversal scope.**

When working with complex reactive state in Vue 3, you often need to monitor changes occurring deep within object hierarchies. The `watch` API in the vuejs/core repository provides a robust mechanism for detecting nested data mutations through its deep traversal system implemented in the reactivity package.

## Understanding the `deep` Option in Vue Watch

The `watch` function accepts an options object where `deep` can be either a boolean or a number. According to the `WatchOptions` interface in the source code, this flag controls how thoroughly Vue walks through the reactive graph.

When `deep` is enabled, Vue wraps your original getter function with a traversal mechanism. In [`packages/reactivity/src/watch.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts), lines 36-44 handle this adaptation:

```typescript
if (deep) {
  const baseGetter = getter
  const depth = deep === true ? Infinity : deep
  getter = () => traverse(baseGetter(), depth)
}

```

This replacement ensures that every time the watcher evaluates its source, it traverses the entire object tree (or to the specified depth), registering dependencies on every nested property encountered.

## How the `traverse` Function Detects Nested Changes

The core traversal logic resides in the `traverse` function within [`packages/reactivity/src/watch.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts) (lines 31-66). This utility recursively visits refs, arrays, plain objects, Maps, and Sets while respecting depth limits to prevent infinite recursion.

The function signature and key logic look like this:

```typescript
export function traverse(value, depth = Infinity, seen?) {
  // Stop when depth exhausted or non-object
  if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  // …handles Ref, Array, Set/Map, PlainObject, Symbol properties…
}

```

When `traverse` visits a property, it triggers the getter of that reactive property, which automatically registers the watcher as a dependency. Consequently, any future mutation to that property—no matter how deeply nested—will notify the watcher and trigger your callback.

## Practical Examples for Detecting Nested Changes

### Deep Watch on Reactive Objects

The most common use case involves watching a reactive object with arbitrary nesting:

```typescript
import { reactive, watch } from 'vue'

const state = reactive({
  user: {
    name: 'Alice',
    address: { city: 'Paris', zip: '75000' }
  }
})

// Enable deep watching to catch any nested mutation
watch(
  () => state,
  (newVal, oldVal) => {
    console.log('Nested change detected:', newVal)
  },
  { deep: true }
)

// These mutations will trigger the watcher
state.user.address.city = 'Lyon'
state.user.name = 'Bob'

```

### Limited Depth Watching

For performance optimization, you can specify exactly how many levels deep Vue should traverse:

```typescript
watch(
  () => state,
  (newVal) => console.log('Change detected up to depth 2'),
  { deep: 2 }  // Only traverse 2 levels deep
)

```

This corresponds to passing `depth === 2` to the internal `traverse` function, preventing unnecessary dependency tracking on deeply buried properties that you don't need to monitor.

### Watching Refs with Nested Data

When using `ref` instead of `reactive`, you still need `deep: true` to detect mutations inside the object:

```typescript
import { ref, watch } from 'vue'

const data = ref({
  items: [{ id: 1 }, { id: 2 }]
})

watch(data, (newVal) => console.log('Ref updated'), { deep: true })

// This mutation triggers the watcher
data.value.items[0].id = 42

```

## Performance Considerations for Deep Watching

While `deep: true` provides comprehensive change detection, it comes with computational overhead. The `traverse` function must walk every property in your object tree during each evaluation, which can become expensive with large datasets.

Consider these optimization strategies:

- **Use specific getters** when possible: Instead of watching an entire object with `deep: true`, watch specific nested properties using computed getters like `() => state.user.profile`.
- **Limit traversal depth**: Use a numeric `deep` value to restrict how far Vue walks into your data structure.
- **Avoid watching massive collections**: Deep watching a Map with thousands of entries or a massive array will trigger traversal of every element.

## Summary

- Enable **deep watching** in Vue by setting `{ deep: true }` in your `watch` options to detect mutations at any nesting level.
- The internal **`traverse`** function in [`packages/reactivity/src/watch.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts) recursively walks through refs, arrays, objects, Maps, and Sets to register dependencies.
- Use a **numeric depth value** (e.g., `{ deep: 2 }`) to limit traversal scope and improve performance on large objects.
- For **specific nested properties**, consider using a computed getter instead of deep watching the entire object to minimize reactivity overhead.

## Frequently Asked Questions

### Does Vue watch detect nested changes by default?

No, Vue's `watch` does not detect nested changes by default. Without the `deep` option, the watcher only triggers when the watched reference itself changes (i.e., when you assign a new object to the variable). To detect property mutations inside an object, you must explicitly set `{ deep: true }` or provide a numeric depth limit.

### What is the difference between deep: true and deep: number?

Setting `deep: true` instructs Vue to traverse the entire object tree with infinite depth (internally represented as `Infinity`), ensuring every nested property is tracked. Providing a number like `deep: 2` limits the traversal to exactly that many levels deep, which improves performance by avoiding dependency tracking on deeply buried properties that you don't need to monitor. Both approaches use the same internal `traverse` function in [`packages/reactivity/src/watch.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts).

### Can I watch specific nested properties without using deep?

Yes, you can watch specific nested properties without enabling deep watching by using a getter function that returns exactly the value you want to observe. For example, `watch(() => state.user.profile.name, (newName) => ...)` will trigger only when that specific string changes, without needing to traverse the entire `state` object. This approach is significantly more performant than deep watching large objects.

### Where is the traverse function implemented in Vue?

The `traverse` function is implemented in [`packages/reactivity/src/watch.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts) within the vuejs/core repository. This utility function handles the recursive walking of reactive data structures including refs, arrays, plain objects, Maps, and Sets. It accepts parameters for the value to traverse, a depth limit, and an optional `seen` Set to handle circular references, returning the original value after registering all encountered properties as dependencies.