'Why Vue3 Composition API - watch’s option deep do not work in reactive, but work in reactive getter?

  1. Arrow function's deep option is work.
  2. Raw Reactive Object's deep option is not work.

It looks like a bug, why watch’s option deep do not work in reactive, but work in reactive getter?

1.Code

setup() {
    const state = reactive({
      id: 1,
      attrs: {
        date: new Date()
      }
    })

    watch(state, (val, prevVal) => {
      console.log('non-deep', val, prevVal)
    })

    watch(
      () => state,
      (val, prevVal) => {
        console.log('non-deep getter', val, prevVal)
      }
    )

    watch(
      state,
      (val, prevVal) => {
        console.log('deep', val, prevVal)
      },
      { deep: true }
    )

    watch(
      () => state,
      (val, prevVal) => {
        console.log('deep getter', val, prevVal)
      },
      { deep: true }
    )

    const changeDate = () => (state.attrs.date = new Date())

    return {
      state,
      changeDate
    }
  }

2.The console logs

non-deep Proxy {id: 1, attrs: {…}} Proxy {id: 1, attrs: {…}}
deep Proxy {id: 1, attrs: {…}} Proxy {id: 1, attrs: {…}}
deep getter Proxy {id: 1, attrs: {…}} Proxy {id: 1, attrs: {…}}


Solution 1:[1]

The Vue official document already points out that:

When you call watch() directly on a reactive object, it will implicitly create a deep watcher - the callback will be triggered on all nested mutations

The Answer:

  • When you pass a reactive object (not a getter) into the watch function, the deep option will be ALWAYS true.
  • When you pass a getter return a reactive object into the watch function, the deep should respect your input options and works as expected.

Extended:

Extended for cases: Ref, ComputedRef

This is the code inside the watch function from the Vue source code:

if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(isReactive)
    getter = () =>
      source.map(s => {
        ...
      })
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    }
  ...
}

As we can see:

  • The reactive source will be wrapped in a getter and the deep option will be always true
  • The Ref | ComputedRef source will be wrapped in a getter that returns source.value and the deep option is the value you passed in. Note that, the value of ComputedRef will be replaced each calculation so the watch function will be trigger after any change of ComputedRef regardless deep option
  • If you use a getter (function) as the source, Vue will use that getter directly and the deep option is the value you passed in

You might ask what kind of my variables? Here is the answer:

const reactiveState = reactive({
  id: 1,
});
// reactiveState is reactive

const refState = ref({
  id: 1,
});
// refState is a Ref
// refState.value is reactive


const computedState = computed(() => {
  return {
    id: reactiveState.id,
  };
});
// computedState is a ComputedRef
// computedState.value is just a raw object without reactive. 
// so calling watch(() => computedState.value, callback) will NOT work

See this code example to verify what I said (you should open the dev tool to see the console logs)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Duannx