Skip to content

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

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

Closed
yurivish opened this issue Jan 31, 2023 · 8 comments
Labels
🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. has workaround A workaround has been found to avoid the problem

Comments

@yurivish
Copy link

yurivish commented Jan 31, 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.

@yurivish yurivish changed the title Triggering a ref passed as a prop does not appear to work BugL Triggering a ref passed as a prop does not cause an update in the nested component Jan 31, 2023
@yurivish yurivish changed the title BugL Triggering a ref passed as a prop does not cause an update in the nested component Possible bug: Triggering a ref passed as a prop does not cause an update in the nested component Jan 31, 2023
@edison1105 edison1105 added 🐞 bug Something isn't working 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. has workaround A workaround has been found to avoid the problem labels Feb 1, 2023
@edison1105
Copy link
Member

as a workaround

<Canvas :data='[...data]' />

@yurivish
Copy link
Author

yurivish commented Feb 1, 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)?

@yurivish yurivish changed the title Possible bug: Triggering a ref passed as a prop does not cause an update in the nested component Triggering a ref passed as a prop does not cause an update in the nested component Feb 1, 2023
@edison1105
Copy link
Member

edison1105 commented Feb 1, 2023

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

@skirtles-code
Copy link
Contributor

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, vuejs/docs#849 (comment), 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.

@LinusBorg LinusBorg removed the 🐞 bug Something isn't working label Feb 1, 2023
@LinusBorg
Copy link
Member

I agree with @skirtles-code

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

@yurivish
Copy link
Author

yurivish commented Feb 1, 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.

@skirtles-code
Copy link
Contributor

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

@yurivish
Copy link
Author

yurivish commented Feb 1, 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. :)

@github-actions github-actions bot locked and limited conversation to collaborators Sep 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. has workaround A workaround has been found to avoid the problem
Projects
None yet
Development

No branches or pull requests

4 participants