Subscribe on changes!

ref is called as a reactive parameter and effectFn is triggered multiple times when updated

avatar
Jul 26th 2022

Vue version

3.2.37

Link to minimal reproduction

https://sfc.vuejs.org/#eNp9kE1qwzAQha8yaGMbHIl0aZxAT9ALaOOacepg/TAauQvju3ccpd01AoGe5tMT723qPUa9ZlSd6tNIc2RIyDlerZ9dDMSwEQ4jzyu2QDi1gNOEI+8wUXBQydPK+jH4xEBwEaTAtbD1uWmsL3xdN3C5wmY9wEGHBfUSbnVFXSXGeh2WjELvso2Bj8wxcwcP8buog7P1T1b+evsXFbIMX+relMSSVQSji8vAKKo3f0K1qvRwckPU9xS8NPVIYZ+DZFVXch130sehrfpijqkzJk3j0e896UA3IydN2fPsUGNyp08K3wlJjK06LHZpQO0/O++Bng==

Steps to reproduce

Open the browser console to view the output.

What is expected?

When assigning a value to r.value, effectFn should be called only once.

What is actually happening?

effectFn was triggered three times and the output was printed three times.

System Info

System:
    OS: macOS 12.2
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 1.47 GB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 14.18.3 - ~/.nvm/versions/node/v14.18.3/bin/node
    Yarn: 1.22.18 - ~/.nvm/versions/node/v14.18.3/bin/yarn
    npm: 6.14.15 - ~/.nvm/versions/node/v14.18.3/bin/npm
  Browsers:
    Chrome: 103.0.5060.134
    Safari: 15.3
  npmPackages:
    vue: ^3.2.37 => 3.2.37

Any additional comments?

Code behaviour does not match type. The reactive function type is:

export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

When target is ref, the target should be returned directly, but instead ref is actually wrapped as responsive, resulting in dependencies being collected on all three properties of value and _value of ref. Eventually, when the value value is updated, it is also updated internally for _value, resulting in effectFn triggering three times.

avatar
Jul 27th 2022

double triggering the effect, might be something that can be fixed, but perhaps with a different approach.

Returning the ref when you pass it through reactive() might not be expected or desired.

let myRef = ref(0)
const myReactive = reactive(myRef)
console.log(isReactive(myReactive)) // actual:false.  expected:true
console.log(myReactive === myRef) // true
avatar
Jul 28th 2022

https://github.com/vuejs/core/blob/a95554d35c65e5bfd0bf9d1c5b908ae789345a6d/packages/reactivity/src/computed.ts#L55-L64 @lidlanca There is also handling of duplicate triggers in computed, but this does not seem to fix the problem, it only solves the dependency collection on the _value attribute, but the value attribute still collects dependencies and the number of duplicate triggers is only reduced by one.

Yes, I realise that handling it in reactive() is not a good practice, and perhaps it is the right thing to do in get() to determine whether or not to collect dependencies via isRef(toRaw(target)).

avatar
Jul 28th 2022

@lidlanca I don't have such a usage scenario, I was struggling to understand the reactivity source code and when I saw the type signature of the reactive(), I realized that there was no associated processing logic.

export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

return it when type T is Ref

Then I saw a similar treatment in computed, but it doesn't seem to have solved the problem.

This was my original intention in asking the question and the PR.

There are many different ways of using the user code and if this is not dealt with, then I think it is only a matter of time before a bug appears.

avatar
Jul 28th 2022

the types seem to be inferred correctly ( unless I miss understand your point) image

there is no reactive type per say, as it is basically a transparent proxy with the exception of nested refs that get unwraped to their inner type)

when someone wrap a ref in a reactive proxy, they should expect the cost and behavior associated with 2 layers of reactive tracking.

const r = ref(0)
const r2 = reactive(r)

// here the effect observes 2 reactive accesses   r.value and r2.value
// that is why you would expect the effect  to be triggered  **twice** when `r2.value`  is mutated
// and only once when `r.value` is mutated directly.

effect(()=>{ console.log(r2.value)})  

r.value++ // logs 1
r2.value++ // logs 2 2 2

the main problem here, is that it is being triggered 3 times instead of 2 times. because the reactive tracks the ref internal _value access. and so the effect is called for r.value r2.value and r._value