Subscribe on changes!

ref in v-for does not trigger watchEffect inside defineComponent

avatar
Jun 23rd 2022

Vue version

3.2.37

Link to minimal reproduction

https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdD5cbmltcG9ydCB7IHJlZiwgd2F0Y2hFZmZlY3QsIHdhdGNoLCB1bnJlZiwgZGVmaW5lQ29tcG9uZW50IH0gZnJvbSAndnVlJ1xuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29tcG9uZW50KHtcbiAgc2V0dXAoKSB7XG4gICAgY29uc3QgY291bnQgPSByZWYoNSk7XG4gICAgY29uc3QgaW5wdXRSZWYgPSByZWYoKTtcbiAgICBjb25zdCBvdXRwdXQgPSByZWYoJycpO1xuXG4gICAgd2F0Y2hFZmZlY3QoKCkgPT4gb3V0cHV0LnZhbHVlID0gaW5wdXRSZWYudmFsdWUubWFwKGVsPT4gZWwuaW5uZXJUZXh0KSlcblx0XHRyZXR1cm4ge2NvdW50LCBpbnB1dFJlZiwgb3V0cHV0fTtcbiAgfVxufSk7XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aW5wdXQgdi1tb2RlbC5udW1iZXI9XCJjb3VudFwiIHR5cGU9XCJyYW5nZVwiIG1pbj1cIjFcIiBtYXg9XCIxMFwiIC8+XG4gIDxkaXYgdi1mb3I9XCJudW0gaW4gY291bnRcIiByZWY9XCJpbnB1dFJlZlwiPlxuICAgIHt7IG51bSB9fVxuICA8L2Rpdj5cbiAge3tvdXRwdXR9fVxuPC90ZW1wbGF0ZT4iLCJpbXBvcnQtbWFwLmpzb24iOiJ7XG4gIFwiaW1wb3J0c1wiOiB7XG4gICAgXCJ2dWVcIjogXCJodHRwczovL3NmYy52dWVqcy5vcmcvdnVlLnJ1bnRpbWUuZXNtLWJyb3dzZXIuanNcIixcbiAgICBcInZ1ZS9zZXJ2ZXItcmVuZGVyZXJcIjogXCJodHRwczovL3NmYy52dWVqcy5vcmcvc2VydmVyLXJlbmRlcmVyLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSJ9

Steps to reproduce

Broken

Notice that this code is within defineComponent and within a setup() function

What is expected?

Working

Exact same code, but within a <script setup> and obviously doesn't 'return' the refs.

What is actually happening?

When you move the range handle, you can see that is correctly adds/removes divs to the DOM

Within <script setup> the watchEffect triggers for each adding/removal of dom elements to inputRef as the range slider is triggered, but within defineComponent() there is an initial triggering, but as you move the range slider, it never triggers again on updates.

System Info

System:
    OS: Windows 10 10.0.19044
    CPU: (8) x64 Intel(R) Xeon(R) CPU E5-1620 v3 @ 3.50GHz
    Memory: 6.94 GB / 15.92 GB
  Binaries:
    Node: 16.6.0 - C:\Program Files\nodejs\node.EXE
    npm: 7.19.1 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 102.0.5005.115
    Edge: Spartan (44.19041.1266.0), Chromium (102.0.1245.44)
    Internet Explorer: 11.0.19041.1566

Any additional comments?

All I really wanted to do was ensure ref for v-for was really working as expected. I didn't know this big hoohar would happen.

Also things to note. If you pre-populate inputRef with an array, this array is thrown away and replaced with another array.. but it will continue to use that array thereafter.

Not really a problem for me. Just unexpected behaviour.

avatar
Jun 23rd 2022

Here's why this is happening:

in <script setup>:

  • the generated render function can pass the ref itself to the vnode, and
  • when the ref's value is set, we read the current value in the process, thereby registering it as a reactive dependency.

in normal setup():

  • the generated rener function passes the ref's name as a string to the vnode, and
  • when setting the ref, it's set via instance.setupState, but we never read its value from there - it's read from instance.refs, which is not reactive. So it's not registered as a render dependency.

https://github.com/vuejs/core/blob/25f7a16a6eccbfa8d857977dcf1f23fb36b830b5/packages/runtime-core/src/rendererTemplateRef.ts#L87

First possible solution coming to mind: Read the refs previous value from instance.setupState if present, instance.refs otherwise.

const existing = _isString ? hasOwn(setupState, ref) : setupState[ref] ? refs[ref] : ref.value
avatar
Jun 23rd 2022

Here's why this is happening:

in <script setup>:

  • the generated render function can pass the ref itself to the vnode, and
  • when the ref's value is set, we read the current value in the process, thereby registering it as a reactive dependency.

in normal setup():

  • the generated rener function passes the ref's name as a string to the vnode, and
  • when setting the ref, it's set via instance.setupState, but we never read its value from there - it's read from instance.refs, which is not reactive. So it's not registered as a render dependency.

https://github.com/vuejs/core/blob/25f7a16a6eccbfa8d857977dcf1f23fb36b830b5/packages/runtime-core/src/rendererTemplateRef.ts#L87

First possible solution coming to mind: Read the refs previous value from instance.setupState if present, instance.refs otherwise.

const existing = _isString ? hasOwn(setupState, ref) : setupState[ref] ? refs[ref] : ref.value

Great👍, it can be fixed.

const existing = _isString ? hasOwn(setupState, ref) ? setupState[ref] : refs[ref] : ref.value