Subscribe on changes!

Reactivity breaks for interdependent properties in slots

avatar
Feb 1st 2023

Vue version

3.2.45

Link to minimal reproduction

https://sfc.vuejs.org/#eNqNVE1znDAM/Ssql02mG5heKbttp5f23iMXL3FYUmMztthMh+G/V/IHyyabDy5Ysvzekyxryn4MQ34aZVZmlWtsNyA4ieMASuh2V2fo6mxf664fjEWYwErRYHeSMMODNT3UGR2usyXip+mHtJMXbDE6ByiJ0LsWdrRzFEqZ5OzcLzZpAy2F6sZoh3AvUJAr8d1MUGuAlkGkc6KVN7deDo5We9x5ywEkfgk4CTXKEhzaTrccHei9++uK9lNaBgjmiJ41x2UQ86SgyHMwRkmh+cgZO5KtmOHbuQBQ0vrJWHVP67nW822tqyLcA1WdDJT9oARKsgCqw4hoNHxvVNf8pdvhIuWrRNa2vzeg749pWyWJaNnwWEUAewM41pGAL83dDjY+gQ3lsvHyN5RJ9L3kjQev8HrH8cv+t4aUaFWQzf54qoRpuuSf5+pgofBBMaVz0HJLVM0LfKcMbpeLMVr9W5gqbtM9Sw7C3wGtfFs/1x/wk8Yr+AH7jayuYF8H/5wUvprBh2r38USrYmnDbJuFl37XiyF/dEbT5Ji83rhBA4MAA0GcDtzlR8TBlUXhHhqeCI8uN7YtaJXbUWPXy1y6/u5gzZOTloBDt/CbIMo0R3hKLVIqrsqenkv4nSXO/wENkpyX

Steps to reproduce

If a setter manipulates multiple fields, changes are not detected properly if not all dependent getters are used within the same slot template.

  1. Create a writable computed property (A) where the setter also manipulates a backing field of another computed property (B).
  2. If property (B) is used in a slot template, changes to its backing field by writing to (A) are not reflected in the slot.
  3. Reactivity works properly if both (A) and (B) are used in the slot template

The linked sample demonstrates this and also shows that outside of slots everything works as expected too.

What is expected?

Computed properties should be re-evaluated properly in slot templates even when their dependencies are not used in the same slot.

What is actually happening?

No re-evaluation happens.

System Info

No response

Any additional comments?

Real-world use case:

  • Setters are used to track changes to data (local to a component or even in a Pinia store)
  • A computed "changed" flag shall be used to enabled/disable a save button that is not part of the main component but put into a slot in a toolbar component, or teleported to another location in the overall view
avatar
Feb 1st 2023

If a setter manipulates multiple fields, changes are not detected properly if not all dependent getters are used within the same slot template.

Your description is missing a further detail: "if a setter manipulates multiple nonreactive fields ..."

And that's expected. Vue observes that the setter has been called, but it can't observe that this setter applies changes to two different let variables because that's a nonreactive change, and there's no way in Javascript to observe a change to a let variable.

Solution: turn these two let variables into ref()'s, that could be observed again.

https://sfc.vuejs.org/#eNqNVMmSmzAQ/ZUeXeyp2FC5EnCSyiW556iLjGXMREiUJDyVovj3tBbEeIlnuKBe9F6/ltQj+d732XngpCClqXXbWzDcDj0IJpuKEmso2VHZdr3SFkbQnNW2PfMNro4wwVGrDihBBEpS2g/V9XMky53lKFxCraSx0JkGKgewpuTEhFCUPM+x1vx0nhi3euApdGCWeX8oYT0ClQANR0BuDGv4+tlXaActHUd2ZmLgMG1cGqpKad5fgLG6lY3bsyRX4P9f5jqS+2kuLMA51uh5y3q5a2GeUyPzXinBmXQbr3ki/U1F8BWbGZsFBa5flRYHXE9UTtiiMg/Hh4eFhuVdL5jlaAGU+8FaJeFbLdr6Dx6qa2S2NPrpre2PG/D7rZpGcCRKAY+VB7AHwLHLCHxpVhWsvIAValn58leoJPpueePGO7zecfq8+yVhFlrmaDt/3FXAOF7yT1O515D7pChpSUqHi928wDdC2U26lEqKv4mpdBd750oOhb8DWvqHcF1/wJ9rvIMfsB+ouoN9H/zTXOF/FXyodx8XWubpGpINCbNh27E+ezFK4sAZfb0xgHMGAQNBnCfulp+s7U2R5+ZYuxnyYjKlmxxXmR6kbTuecdNt91q9Gq4RmBL/5iJGjs4z11vN5YFrrh9hXqXe4DpY99ZQyjzR3NBMEkvX7R0+w/BbpE//AJv8z/U=

avatar
Feb 1st 2023

Hi Thorsten, thanks for the fast response. I can understand this, but I'm still curious why this works when both computed properties are used side by side, and also why it works for multiple instances of the same (directly manipulated) property in different places.

I updated your sample with another missing ".value" here: https://sfc.vuejs.org/#eNqNVD2TmzAQ/St7auyb2DBpCTjJpEn6lDQyljEXITGS8E2G4b9nVxL4bBPf0aD90Hv7VtIO7HvXJedesIzltjJN58AK13cguaqLkjlbsl2pmrbTxsEARvDKNWexwdURRjga3ULJEKFkc9oP3XZTJEnJIgpKqLSyDlpbQ0EA65KduJS6ZM9TrLE/yRPjzvRiDh24494fSlgPUCqAWiCgsJbXYv3sK3S9UcSRnLnsBYwbSkNVc5r3Z2CdaVRNey7JBfj/l6mO2f107QigxB39b7mXUol/So38e62l4Io23rLFIu7qgq/Y0tgyyHD9qo084Hos1YiNytNwiHhkaDjRdpI7gRZAvu+d0wq+VbKp/uDRUjuTS7uf3tr+0AG/37qupUCiOeCx0gD2ADj2GoGvzaKAlRewQi0rX/4KlUTfPW/cuMDrHafPu18KJqF5ijb5464MhuGafxzzvYHUJ0VJl6SpGSN28wrfSu0289XUSv6dmXK63jsqORT+Dmjun8Nt/QF/qnEBP2A/ULWAvQz+aarwvwo+1LuPC83T+RqyDQsTYtvyLnmxWuHYGXy9MYDTBgEDQZwqdMtPznU2S1N7rGiSvNhEmzrFVWJ65ZpWJMK2273Rr1YYBC6Zf3MRI0XnWZitEeogjDCPMG9S73AJlt4aSpnmGo3OWWJO3d7hMwy/i/TxH8Ri0kA=

Edit: I can confirm this was the issue with my much more complicated real-world app. Again I'm curious though: reactivity seemingly worked for many, slightly different experiments of mine "auto-magically" with non-ref fields, and only broke down for that specific setup?

avatar
Feb 1st 2023

I'm not sure what Im supposed to look for in that second example.

Generally said:

  • Vue tracks property reads, and re-reruns effects (i.e.: rendering) when those properties are being set.
  • Each component tracks its own reactive dependencies
  • In your example, the first two child components components only track one property each: the first tracks data.isHallo, the second tracks data.message
  • So when you set data.isHallo, only the first one will re-render. The second will not, because data.message has not been set. The fact that you set a let variable that holds the value for the get message getter is unknown to Vue. So the second component will not re-render because as far as it can tell, it's reactive dependencies have not been changed.
  • And likewise, if you set data.message, only the second one will re-render.
  • However, the third one depends on both of those properties, so when any one of those properties' setters is being set, it will update the whole template with the latest data. So if it re-renders because you have set isHallo, then it will read both property's getters in the update, and thus update the dom with both properties' latest values, even though it did not actually detect a change of data.message
avatar
Feb 1st 2023

Thank you so much for taking the time to explain the details! With these I was able to observe the chain of events in the debugger, in particular your first and last bullet points. To confirm, what I was seeing was:

  • When you set a (reactive) property, a render is triggered for all places reading that property, no matter what. This is the reason direct updates of properties work "reactively" even when the backing field is non-reactive, and even across multiple locations where bindings to the same property exist in a template.
  • Re-renders cannot be arbitrarily granular but need to happen along certain boundaries, for example components. If setting a property causes a re-render of an element, sibling elements etc. will be also re-rendered, which effectively re-evaluates the bindings of these siblings too. This is the reason I've seen "reactivity" with non-reactive backing fields in many constellations. In reality, there was no reactivity, but direct updates of other properties with data bindings "nearby" coincidentally refreshed the non-reactive places too.

Sorry about the confusion around my second link. I just added a missing ".value" statement in your fix which broke one of the bindings (just so others who may stumble upon this issue and want to test for themselves have a fully working version).

Thanks again!