ref is called as a reactive parameter and effectFn is triggered multiple times when updated
Vue version
3.2.37
Link to minimal reproduction
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.
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
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))
.
@hubvue
it will probably be best to avoid passing a ref directly to reactive(ref())
I don't see a good reason for this.
do you have a practical use case, where that was necessary to do?
additionally watchEffect
might be useful for this case
https://sfc.vuejs.org/#eNp9UEFqwzAQ/Mqiix2wJdKjcQI99NwP6OK668TBlsRq5RyM/951nJbQ0ggEGu3MLDOzeg1BTwlVperYUh8YInIKR+v6MXhimAmblvsJCyDsCrg23J7fug5bXqAjP0Im+sy61rvIQHAQ3qbIRZDvdzvrHkR5voPDEWbrAFaJH1AP/pRnVGWyQk/NkFAki1xj4D1xSFzBDXwfqmBv3Z0rC1/+pQpzGz7FtdmyS2oBjGMYGkZBtfkBqlBbI+XYBH2J3klntxT2PohWVVuu9U9KWbFVZ+YQK2Ni165NX6L2dDLy0pQc9yNqjGP5Qf4akcTYquLBw8jnhFQSuk8kpGeev6h/fFfbRZpVyxecgKS/
@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
isRef
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.
the types seem to be inferred correctly ( unless I miss understand your point)
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