Subscribe on changes!

Triggering a ref passed as a prop does not cause an update in the nested component

avatar
Jan 31st 2023

Vue version

3.2.45

Link to minimal reproduction

https://sfc.vuejs.org/#eNqlU01v2zAM/SuED7WDOlI/tku+0GLYYbdhWE91gaoxE7uxJUGiYxSB//uoOB9uV+yyiyHxke9RfPQuurdWbBuMJtHML11pCTxSYxeZLmtrHMEOfKGqyrS/cJUCuXK9Rrc/t4qWxffVCpeUgtEPujaNJsyhg5UzNcTMG594vim9Vf6ACNlfg3ScUaYzvTTaE+SKFMwHkonGFh5KTbc3986pt+R6NJpmGuBYwRC6rapCFdKPwy1JRjBfwI6Zqc9TznFK4BeMNxg4OPZ49XR5Gc4hy1QoKrNOhBAMjUL4/OAk1HKsS+H66ooPgycf5JYVKndq4djZiHMzLSX8LkoPrtEe2gI1PJ+5n4GRUm/NBvMUXhoCKnDf7L7QOmPBKu95uGSOo8wNeh0Hw7AO4da4zb7OqxrZnTeR6YFHxx4H7zxPI/Q4k/0GsPd8IaxtpQj5BjCzi4uKprwsF2uawt6JwESoyU9gt+tDIrjbz0m8mlIncQrxCLpuJsNGMc+h9UnImcfhG4NkaCZPelEa9SszrpUVr95oXk42ki0/AD6LWDNEQoxXKNyzqCCyfiJlo+1mLZamlneMSR44lTWOc1Pf3Yob8eWrzEtPw7hAX49fnGk9OlbMonRALjm4RTd2qHN06P4p9iH3neAH7C/RoNnxfvEAzn/HJz9mjqtS40/eCZ889jN8em/fp+b1nP/j39Ck7g+CjnZe

Steps to reproduce

Observe the output – the two lines should be identical, but they are not.

What is expected?

data is a shallowRef holding an array.

There is a setInterval that updates an array element, then triggers the data shallowRef.

The two lines of output should be the same – the first is the direct output of the App template and the second is the output of a nested Canvas component.

What is actually happening?

For some reason, despite the triggerRef calls, the reactivity does not propagate to the Canvas component instance.

image

System Info

No response

Any additional comments?

Curiously, if I pass the prop as :data='[data]' instead of :data='data', then the two stay in sync.

I think this means that triggerRef causes the data prop to re-evaluate, and since each evaluation returns a new array object ([data]) this causes the Canvas component to be re-rendered.

But I think that data changing should be enough to cause the Canvas component to re-render, and it is puzzling why it doesn't.

avatar
Feb 1st 2023

as a workaround

<Canvas :data='[...data]' />
avatar
Feb 1st 2023

Hi @edison1105, thanks for the quick response! Your workaround is a good one for the specific example I gave in the issue, but has performance implications that prevent it from being a workaround in my actual use case, where the data is a large Uint8Array representing the ImageData of a canvas.

One alternative workaround would be to wrap the data array in a new plain object each time, but that would still require creating a new object for each update.

I wonder if there’s a workaround that doesn’t require an allocation each time the image data changes (which can be as often as every frame)?

avatar
Feb 1st 2023

@yurivish Another workaround without shallow copy the array. But not elegant enough

avatar
Feb 1st 2023

I've seen variations on this theme a few times on Vue Land. I added a note about it a few weeks ago in the docs repo, https://github.com/vuejs/docs/issues/849#issuecomment-1382072571, as it's something we might want to consider documenting.

While I can understand why people find it counterintuitive, I don't think there's a bug here. triggerRef() is successfully triggering dependants of the ref's value property. The parent is being forced to re-render as it has a direct dependency on the ref, but there's no reason for the child to re-render.

It's effectively like calling $forceUpdate().

triggerRef() isn't 'deep' on any non-reactive objects it contains. I'm not sure whether such a thing would even be possible, as we've no way of knowing exactly where non-reactive objects are used.

Maybe there is some magical way to make this work, but I suspect it'd need a new API to do it, to avoid breaking changes to the existing APIs. Ultimately we might just need to document this better.

But I think that data changing should be enough to cause the Canvas component to re-render, and it is puzzling why it doesn't.

The parent is re-rendering, which updates the props on the child. But the props on the child are considered equal from a === perspective, so the child won't re-render.

avatar
Feb 1st 2023

I agree with @skirtles-code

When working with nonreactive data, it's better to then work with immutable data as @edison1105 suggested.

avatar
Feb 1st 2023

Thank you for the great explanation, @skirtles-code. The issue you linked containing explanations of problems with reactivity is also very useful.

I think my mistake here was actually in not understanding how props are passed – I knew that that triggering a ref without changing its value would cause re-evaluation only "one level" down – ie. any direct dependents would be re-evaluated, with the usual rules being followed beyond that – but thought that I could pass the ref down into the child component directly, where those changes could be directly observed by the child.

Here's an example I made in the SFC playground that clarified things for me. It explores the various options for passing a ref prop.

It looks like what's happening is that Vue will unwrap any ref values passed as props inside the render function of the parent component.

  • Using x.value if x is a ref
  • Using unref(x) if the type of x is not known to the compiler to be a ref

So the only way of giving the child component access to the ref from the parent is to use a function that returns it, and to define that function outside the template so that the unref transformation does not get applied.

By the way, thanks for your work on the Vue docs – they're really good and a big part of why I got interested in exploring Vue in the first place.

avatar
Feb 1st 2023

@yurivish The Playground you linked is slightly misleading. Both the parent and child templates will unwrap refs, so it isn't immediately clear in each example whether the unwrapping is occurring in the parent or the child.

The process of passing a prop doesn't directly unwrap anything.

Top-level refs are unwrapped automatically whenever they are used in the template. So if a ref is wrapped in a plain object it will not be unwrapped, Playground example.

{{ }} interpolation will also unwrap a ref if it is the final value of the expression. This can give the impression that nested refs are automatically unwrapped, but in general they aren't.

See also https://vuejs.org/guide/essentials/reactivity-fundamentals.html#ref-unwrapping-in-templates.

avatar
Feb 1st 2023

@yurivish The Playground you linked is slightly misleading. Both the parent and child templates will unwrap refs, so it isn't immediately clear in each example whether the unwrapping is occurring in the parent or the child.

I was using the output of the handy JS inspector in the playground to see where the unwrapping was happening:

return (_ctx, _cache) => {
  return (_openBlock(), _createBlock(Canvas, {
    data: data.value,
    getData: getData,
    inlineGetData: () => data.value,
    wrappedData: wrappedData,
    inlineWrappedData: { data: data.value },
    renamedData: _unref(renamedData)
  }, null, 8 /* PROPS */, ["data", "inlineGetData", "inlineWrappedData", "renamedData"]))
}
}

Thank you for the corrections and clarifications (and the example)! Learning a lot today. :)