Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
hubvue opened this issue Jul 26, 2022 · 6 comments · Fixed by #6376
Closed

Comments

@hubvue
Copy link

hubvue commented Jul 26, 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.

@lidlanca
Copy link
Contributor

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

@hubvue
Copy link
Author

hubvue commented Jul 28, 2022

get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}

@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)).

@lidlanca
Copy link
Contributor

lidlanca commented Jul 28, 2022

@hubvue
Copy link
Author

hubvue commented Jul 28, 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.

@lidlanca
Copy link
Contributor

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

@yyx990803
Copy link
Member

This is now triggering twice in 3.5 - once for ref being mutated, and once for .value being mutated as a proxied property. IMO triggering twice is the expected behavior.

@github-actions github-actions bot locked and limited conversation to collaborators Sep 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants