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

$refs should be reactive to be able to use them in computed properties #3842

Open
TheDutchCoder opened this issue Oct 3, 2016 · 52 comments
Open

Comments

@TheDutchCoder
Copy link

Now that refs are no longer reactive, we need to emit events and payloads from child components.
This is fine and works well, but it becomes hard to maintain in the following scenario:

When you create a custom form input component, that basically wraps an input into it's own re-usable component, we can no longer access the input's value (and other props) reactively in e.g. a computed prop in the parent.

Take the following use-case:

<my-input ref="email"></my-input>
<my-input ref="password"></my-input>
<p>This is {{ isValid ? 'valid' : 'invalid' }}</p>

Previously we could create a computed prop like this:

isValid() {
  return this.$refs.email.isValid && this.$refs.password.isValid
}

Since refs don't work reactively anymore, we now need to use $emit to inform the parent of changes.
However, these emits are handled by methods, so we need a separate handler per input to deal with the events.

Something like:

<my-input @valid="handleValidEmail"></my-input>
<my-input @valid="handleValidPassword"></my-input>

handleValidEmail(value) {
  this.email = value
  this.emailIsValid = true
},

handleValidPassword(value) {
  this.password = value
  this.passwordIsValid = false
}

You could refactor this a bit, but it's not a nice way of dealing with this and I'm assuming using forms is quite a common occurence for a lot of devs.

Can we think about a better solution to something like this?

@fnlctrl
Copy link
Member

fnlctrl commented Oct 4, 2016

How about using one handler with multiple parameters?

<my-input @valid="handleValid"></my-input>
<my-input @valid="handleValid"></my-input>

handleValid(value, type) {
  this[type]= value
  this[type + 'Valid'] = true
},

inside components:

this.$emit('valid', value, 'email')
...

this.$emit('valid', value, 'password')

@TheDutchCoder
Copy link
Author

Because you can have multiple of the same type of fields.
Maybe I could use their ID's in that case instead?

@itsMapleLeaf
Copy link
Contributor

Try something like this:

<template>
  <form>
    <my-input v-model='fields.email'></my-input>
    <my-input v-model='fields.password'></my-input>
    <p>This is {{ isValid ? 'valid' : 'invalid' }}</p>
  </form>
</template>

<script>
export default {
  data () {
    return {
      fields: {
        email: { value: '', valid: false },
        password: { value: '', valid: false },
      }
    }
  },
  computed: {
    isValid () {
      return Object.values(this.fields).every(field => field.valid)
    }
  }
}
</script>

Where the input components v-model both the value of the input, and the validity status.

@TheDutchCoder
Copy link
Author

TheDutchCoder commented Oct 4, 2016

So this is where I get confused.
How does the model update the valid part? I thought it only update the value?

this.$emit({
  value: event.target.value,
  valid: someValidationMethod(this)
})

Or something?
If that works it would be good (certainly not great).

The main issue I have with this, is that I need all those fields in my state, whereas previously I didn't, because I could just use the refs.

Having them in my state is a pretty major pain in the ass, because it clutters my state without a good reason.

@LinusBorg LinusBorg added the 2.0 label Oct 4, 2016
@LinusBorg
Copy link
Member

The main issue I have with this, is that I need all those fields in my state, whereas previously I didn't, because I could just use the refs.

@TheDutchCoder This is inconvenient indeed. :(

@yyx990803 Do you have something in mind for situations like these?

@TheDutchCoder
Copy link
Author

Just to add some context, this is what I could do previously:

computed: {
  isReady() {
    return this.$refs.email.isValid && this.$refs.password.isValid
  }
}

But now I need to add additional state and a handler to deal with this:

data() {
  return {
    email: { value: '', isValid: false },
    password: { value: '', isValid: false }
  }
}

// Some method that dynamically handles input changes

// Computed prop
computed: {
  isReady() {
    return this.email.isValid && this.password.isValid
  }
}

Mainly the extra (not very useful) state and the handler are a bit of a pain, would be great if there could be some alternative to what used to be $refs that remain reactive. Not sure what the implications would be and why it was deprecated, I'm just trying to illustrate a use case when the old $refs were very useful.

@LinusBorg
Copy link
Member

LinusBorg commented Oct 4, 2016

Would it be ok to rename the issue to something like: "$refs should be reactive to be able to use them in computed properties"?

@TheDutchCoder
Copy link
Author

Sure go for it! Thanks for the discussion guys, it;s appreciated.
Vue is an amazing framework and the open conversations around it make it only better ;)

@LinusBorg LinusBorg changed the title We need some replacement for refs $refs should be reactive to be able to use them in computed properties Oct 4, 2016
@itsMapleLeaf
Copy link
Contributor

itsMapleLeaf commented Oct 4, 2016

So this is where I get confused.
How does the model update the valid part? I thought it only update the value?

If a component emits an input event while accepting a value prop, v-model will update the value in the parent. So your my-input component would need to do something like this:

<template>
  <input :value='value.value' @input="onInput($event)">
</template>

<script>
...
  props: ['value'],
...
  onInput(event) {
    this.$emit({ value: event.target.value, valid: this.someValidatorFunction() })
  }
...
</script>

I imagine there's a better way, though. Without reactive refs, that is. Perhaps using mixins, somehow...

I'm still for reactive refs, though. It's a super useful thing to have in general.

@TheDutchCoder
Copy link
Author

Alright, at least your example works.
Now with the added state, the real issue is the combination with Vuex.

Since the Vuex getters aren't yet available when the state is defined, we can't use them to populate the values.

We could use mounted or something, but again, that's quite dirty.

@TheDutchCoder
Copy link
Author

Here's two repos that compare the two scenarios:

Vue1: https://jsfiddle.net/okv0rgrk/8330/
Vue2: http://jsfiddle.net/5sH6A/744/

I hope this clearly illustrates the current problem. The biggest issue right now is the fact that you can't use computed props in the child component anymore. Even this.$nextTick doesn't work in the emit event, because the v-model hasn't updated yet.

@pdemello
Copy link

I've also found reactive $refs to be useful in unit tests.... like so:

    var component = vm.$refs.testComponent
// .....   do something that's expected to trigger a modification of the DOM .....
    vm.$nextTick(() => {
      expect(component.value).to.equal('Something')
      done()
    })

Is there some suggested way to replace this sort of pattern? Otherwise it would be nice for this to work again.

@blake-newman
Copy link
Member

I have to agree with @TheDutchCoder

Using refs is an easy way for parent to read child information. Which inadvertently makes $refs seem useless without reactivity.

I commonly also use this practice, to check the validity of ref components to validate the parent component.

@paulpflug
Copy link

Do you have access to my-input? Then you could create a $emit('validity-changed',this.isValid)
and where you use it:

<my-input @validity-changed="isValid=$event"></my-input>
<my-input @validity-changed="isValid=$event"></my-input>
<p>This is {{ isValid ? 'valid' : 'invalid' }}</p>
// with
data: function() {return {isValid:true}}

for a normal Input I would try a computed setter:

template: "<input v-model='username'></input>",
computed: {
  username: {
    get: function() { return this.name }
    set: function(val) {
      this.valid = this.validate(val)
      if (this.valid) {
        this.name = val
      }
    }
  }
},
data: function() {
  return {name:"name", valid: true}
}

I'm using ref only for testing (checking out some component instance) or for calling methods on child components.
All data stuff I do with computed, data, methods and/or watch - and I never had a problem with it.

@yyx990803 yyx990803 added 2.x and removed 2.0 labels Nov 24, 2016
@gustogummi
Copy link

Using Vue in Meteor, I really miss this feature. But in Meteor's default frontend Blaze, $ref was available as a part of 3rd party package, not as part of the core. So if it's not possible in the core in Vue2, at least having it as a plugin would be a valuable option.

@controversial
Copy link

controversial commented Apr 15, 2017

This is indeed inconvenient. I'm trying to use computed properties to control the appearance of an element based on a property of two of its siblings:

<h1 v-if="showTitle">codus</h1>
<modal ref="loginModal">
  ...
</modal>
<modal ref="signupModal">
  ...
</modal>
computed: {
  showTitle() {
    return !(this.$refs.loginModal.shown || this.$refs.signupModal.shown);
  },
},

I believe this is a legitimate use case and it'd be nice to be able to do something like this.

However, it seems this.$refs is empty when the computed property is executed. As a simple test, I included the following in my computed property:

console.log(this.$refs);
setTimeout(() => console.log(this.$refs), 1000);

{} is logged, and a second later the populated refs object is logged.

Thoughts?

@andymerskin
Copy link

andymerskin commented Apr 20, 2017

Another use case I'm running into is testing whether a component is focused to use it to control state:

<template>
  <div class="my-component-wrapper">
    <div class="my-component" tabindex="0"></div>
  </div>
</template>
computed: {
  hasFocus() {
    return this.$refs.myElement === document.activeElement
  }
}

Right now, I'm listening for focus and blur, with separate methods to set a data property called focused (true/false) and while it works, it's a pain to implement while the above solution is where computed properties shine.

@LinusBorg
Copy link
Member

Well, that use case would not even work if $refs was reactive, because no refs changed in any way, and neither did their data, only the DOM changed.

@andymerskin
Copy link

Ah good call, technically document.activeElement is the only thing changing here. Damn. 😞

@LinusBorg
Copy link
Member

Can't recall if the focus event bubbles, but if it does, register an event listener in created() and save the target element in the component's data? That would be reactive, and the computer prop could re-evaluate.

@LinusBorg
Copy link
Member

On second thought that could lead to unnecessary rerenders.

@kenberkeley
Copy link

The simplest workaround is using $watch: https://jsfiddle.net/kenberkeley/9pn1uqam/

@tomwang1013
Copy link

tomwang1013 commented Aug 15, 2017

I also find situations when reactive $refs is handy. Without it, you must resort to more complex solution, e.g. emit/vuex, to get the children's state.

Anyway, i think getting the children's state from parent is natural and straightforward. Without reactive feature, $refs is actually useless.

@AndreKR
Copy link

AndreKR commented Oct 28, 2017

Of course everyone tries to design components that way. But when you have sibling components like in my case, where a button lives in a different slot of a parent, I think $refs to components actually make the code cleaner instead of cluttering it with meaningless container components which hold part of the state of their children.

@maxnorth
Copy link

It's true, I see your point as well. I imagine the middle ground would be something like distinguished public and private properties/computeds/methods. That way a component's interface could remain well-defined and components could take useful references on their children.

@maxnorth
Copy link

The more I think about it, the more I kinda like that idea. Could maintain compatibility by making everything public by default, but if someone wants to privatize data they could specify which fields are part of the public interface by using a property like "public": ["field1", "computed2", "method3"]. Then component refs could be proxy objects that only contain the exposed members.

@kamerat
Copy link

kamerat commented Jan 9, 2018

Any updates on this?

@Akryum
Copy link
Member

Akryum commented Jan 9, 2018

But when you have sibling components like in my case, where a button lives in a different slot of a parent,

You can use a "bus" instance (for example const bus = new Vue()) or use dedicated state management solutions like vuex (more info).

with meaningless container components which hold part of the state of their children.

This is not that meaningless and may be a good solution to make this maintainable. What is your use case?

@AndreKR
Copy link

AndreKR commented Jan 9, 2018

@Akryum
Copy link
Member

Akryum commented Jan 9, 2018

Here is a solution with provide and inject, without using $refs, only with reactive data: https://jsfiddle.net/Akryum/ekf2oyef/
Documentation of provide/inject

@posva posva removed the 2.x label Jan 10, 2018
@mikeevstropov
Copy link

mikeevstropov commented Feb 15, 2018

Hi. An expression by key attribute does not require any reactivity and will be computed by setInterval or something else by "internal Vue logic", is it? So why is it always "Child is an undefined"?

<span :key="typeof $refs.myChild">
    Child is an {{ typeof $refs.myChild }}
</span>
                        
<my-component ref="myChild"/>

Thank you.

@trusktr
Copy link

trusktr commented May 8, 2018

If $refs were reactive, they could also be watched. 👍

@kryvonos-v
Copy link

Is there any progress with this feature?

@anthonygore
Copy link

anthonygore commented Sep 16, 2018

I see the problem reactive refs solve. But I also think there's a good reason why they're not reactive. As it says in the docs, refs are "only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties."

As we all know, it's an important pattern that components are kept isolated and communicate through the props and events interface. Refs only exist for edge cases where this pattern is insufficient.

Once refs are reactive, it'll allow for an anti-pattern where refs are used instead of events which will result in components being coupled.

So I agree that it's messy making forms with Vue where the inputs are in separate components, and no doubt reactive refs would be convenient here. I just think it's too easy for them to be abused, so a better solution should be considered.

@hdodov
Copy link

hdodov commented Oct 25, 2018

For me, non-reactive $refs makes is harder accomplish things with DOM stuff that isn't reactive (by nature).

Right now, I'm working on a dropdown component and I use getBoundingClientRect() of the element holding the content to improve UX. Because refs isn't reactive, I need to watch when the dropdown becomes active (and is therefore mounted) and make the DOM call then. This gets even more complicated when that component is extended and the things determining when the dropdown is active, change. So I have a computed property isActive, my element has v-if="isActive" and I have to watch isActive where I update make the DOM call in $nextTick...
If refs were reactive, I could simply watch them, check when the dropdown is available, and do whatever I've got to do. I don't have to care what other components extending the base component do, I only care when the element is present. Non-reactive refs make things complicated for no reason.

I agree with @anthonygore

Once refs are reactive, it'll allow for an anti-pattern where refs are used instead of events which will result in components being coupled.

So I agree that it's messy making forms with Vue where the inputs are in separate components, and no doubt reactive refs would be convenient here. I just think it's too easy for them to be abused, so a better solution should be considered.

Conventions should be followed, no doubt, but doing things the right way is a responsibility of the developer. What a great tool like Vue should do is to give you as much functionality as it can while remaining efficient. People break rules all the time and stripping away useful functionality in an attempt to prevent that is not the right way to handle things, in my opinion.

It's the tool's job to give you options, it's your job to educate yourself and do things right.

@sirlancelot
Copy link

sirlancelot commented Oct 25, 2018

The biggest issue with $refs reactivity is that it's already tied to side-effects of your state. $refs represent output and you shouldn't rely on that output to feed back in to your state because it will almost always result in a circular dependency.

If your problem depends on $refs being reactive, there is always a better solution. Have you looked at the updated() hook? That is where you have access to the latest values stored in $refs. Give it a try and you'll quickly realize the circular dependency it would make.

@mitar
Copy link
Contributor

mitar commented Oct 25, 2018

The biggest issue with $refs reactivity is that it's already tied to side-effects of your state. $refs represent output and you shouldn't rely on that output to feed back in to your state because it will almost always result in a circular dependency.

Why not? You can depend on computed properties? And so if we see $refs as a computed property of a special type, why you could not depend on that?

You can make a cycle with computed properties and you can make $refs cycle. This does not change anything. On the other hand, if updating $refs already does the transformation of the state you want, why not use it?

For example, imagine that I want a counter of how many visible components are displayed as children, where component can contain logic to hide itself or not. I can duplicate this logic and have on the parent component logic which counts how many components should be visible. Or I can simply just count the visible components in $refs. Why would that introduce a cycle is beyond me.

@tmorehouse
Copy link

I've had luck with including a dummy test on a reactive prop/data/computed value, and then returning an element dom property:

<my-input v-model="email" ref="email"></my-input>
<my-input v-model="password" ref="password"></my-input>
<p>This is {{ isValid ? 'valid' : 'invalid' }}</p>
computed: {
  isValid() {
    // dummy test to make this reactive to changes
    if (this.email === this.$el || this.password === this.$el) {
      // this return will never happen, but by 'getting' the v-models values registers
      // this computed prop as reactive to changes in the form input values
      return
    }
    return this.$refs.email.isValid && this.$refs.password.isValid
  }
}

If you need to react to changes in the DOM (i.e. adding new child elements, attribute changes), you can use a MutationObserver (see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) and then set a data value when the element (and/or it's children) changes.

@davidkhess
Copy link

Just to add my own two cents on this, my work around for non-reactive $refs involved using v-bind.sync. That allows me to hold some state in the parent that is updated as needed by the children.

While a $refs approach was more intuitive in my case, v-bind.sync did end up cleaner.

@sirlancelot
Copy link

sirlancelot commented Jan 23, 2019

I've almost completely eliminated my own needs for $refs by leveraging Vue's provide/inject options.

If you have a pair of components that need to be coupled together, you make it available to children through provide(), and then inject it in children that need it.

Consider this simplified RadioGroup component snipped from a real component that I'll be using in production soon:

export default {
  name: "RadioGroup",
  provide() {
    return { $radio: this } // Be careful how you name this to not overlap with Vue.js!!
  },
  data: () => ({
    inputs: []
  },
  methods: {
    register(input) {
      this.inputs.push(input)
      input.$on("hook:beforeDestroy", () => spliceItem(this.inputs, input))
      // Do anything else needed for initialization
    }
  }
}

Then, child components can register with their parent like so:

export default {
  name: "RadioOption",
  inject: {
    $radio: { default: null }
  },
  created() {
    if (!this.$radio) throw new Error()
    this.$radio.register(this)
  }
}

A few benefits come along with this method:

  1. $refs is not used
  2. Better documentation through Dependency Injection
  3. It's vue-devtools inspectable
  4. Best of all: inputs is reactive 🎉

@mesqueeb
Copy link

mesqueeb commented Apr 6, 2019

@sirlancelot I understand what you're doing here. But I don't see where you defined spliceItem. Did you just mean for us to write a function that splices a component after destroy again from the array? Eg. find the index via the component id and splice that index?

I also wanted to ask if you'd recommend $on('hook:beforeDestroy') or $once('hook:beforeDestroy').

@tmorehouse
Copy link

@mesqueeb I would recommend $once over $on as it would ever only be called once.

@sirlancelot
Copy link

Yes, spliceItem() is a simple function to remove an item from an array using Array#splice()

No, you don't need to use $once as the component you are adding it to is being destroyed. Using $once is a waste of CPU cycles.

@posva
Copy link
Member

posva commented Jul 25, 2019

Although I don't totally approve having reactive $refs as it can show a bad design choice in the model structure, I think it has its valid use cases so I created a small plugin to make $refs reactive: https://github.com/posva/vue-reactive-refs. It probably have caveats, so if you found any, please open an issue on the repo 🙂

@dsfx3d
Copy link

dsfx3d commented Oct 22, 2019

@posva why would it be a bad choice to use reactive refs. In your plugin, you've used observable to make the refs reactive. will it affect performance? where might it go wrong?

@davidkhess
Copy link

davidkhess commented Sep 6, 2020

Wanted to share this approach I just stumbled onto.

Again, using a .sync approach or event is clearly recommended and preferred. However sometimes, it's just not enough - sometimes components have methods and variables on them that need to be called or bound to respectively by the parent component (e.g. the Vuetify Calendar component). Or maybe the component is outside of your scope of control and they made a poor decision in that regard.

Regardless, if the component in question does not exist when the parent is mounted (e.g. due to a v-if on the component), then any references to some state on it through this.$refs[component_name]will fail to update in the parent until something else triggers the parent to re-render since $refs is not reactive.

I ran into this case recently and since I didn't see it elsewhere in this thread, I thought I'd post about $forceUpdate here: https://vuejs.org/v2/api/#vm-forceUpdate

What I was able to do was hook on the event that caused my component to mount and then in $nextTick, I called $forceUpdate in the parent. This caused the parent to rerender which then fixed my stale this.$refs[component_name] reference.

UPDATE:

One weakness to this approach is that it forces the component to render, but it doesn't force computeds to recompute. So, if you have a reference in your template via $refs, it will work. If you are relying on a reference in a computed to $refs it will not.

However, I did find this little gem which shows you how to force a computed to update if needed: #214 (comment)

this._computedWatchers.myComputedValue.run();

@zhengxuan0601
Copy link

@posva why would it be a bad choice to use reactive refs. In your plugin, you've used observable to make the refs reactive. will it affect performance? where might it go wrong?

Wanted to share this approach I just stumbled onto.

Again, using a .sync approach or event is clearly recommended and preferred. However sometimes, it's just not enough - sometimes components have methods and variables on them that need to be called or bound to respectively by the parent component (e.g. the Vuetify Calendar component). Or maybe the component is outside of your scope of control and they made a poor decision in that regard.

Regardless, if the component in question does not exist when the parent is mounted (e.g. due to a v-if on the component), then any references to some state on it through this.$refs[component_name]will fail to update in the parent until something else triggers the parent to re-render since $refs is not reactive.

I ran into this case recently and since I didn't see it elsewhere in this thread, I thought I'd post about $forceUpdate here: https://vuejs.org/v2/api/#vm-forceUpdate

What I was able to do was hook on the event that caused my component to mount and then in $nextTick, I called $forceUpdate in the parent. This caused the parent to rerender which then fixed my stale this.$refs[component_name] reference.

UPDATE:

One weakness to this approach is that it forces the component to render, but it doesn't force computeds to recompute. So, if you have a reference in your template via $refs, it will work. If you are relying on a reference in a computed to $refs it will not.

However, I did find this little gem which shows you how to force a computed to update if needed: #214 (comment)

this._computedWatchers.myComputedValue.run();

@heyakyra
Copy link

heyakyra commented Dec 2, 2022

It seems like a bug that even when adding Vue.observable to elements accessed by ref, that a computed property still doesn't detect changes. For example, if I want to watch when all <img> elements are loaded ie. complete is true (since there's no corresponding event afaik), something like this doesn't even work (change loaded to always return true if you want to see the images):

<!-- Include the library in the page -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<!-- App -->
<div id="app">
  <div class="images-loading">
  	<div v-for="src in urls">
  		<img v-show="loaded" :src="src" ref="images" />
      <p v-show="!loaded">Loading…</p>
    </div>
  </div>
</div>
  data: () => ({
    isMounted: false,
  }),
  mounted() {
    const watch = (el) => Vue.observable(el);
    this.$refs.images.map(watch);
    this.isMounted = true;
  },
  computed: {
    loaded() {
      const isLoaded = (image) => image.complete;
      return this.isMounted && this.$refs.images.every(isLoaded);
    },
    urls() {
      return [
        "https://ddz4ak4pa3d19.cloudfront.net/cache/44/c1/44c1b378803bfb4cea3cf36190343de9.jpg",
        "https://ddz4ak4pa3d19.cloudfront.net/cache/61/ed/61ed55ee07ca54d75c63081959fc87d2.jpg",
        "https://gooseberry.blender.org/wp-content/uploads/2015/05/gooseberry_teaser_image.jpg",
        "https://orange.blender.org/wp-content/themes/orange/images/media/gallery/s3_telephone_t.jpg",
        "https://peach.blender.org/wp-content/uploads/rodents.thumbnail.png",
      ]
    }
  }

https://jsfiddle.net/cytduw8k/

Is this due to non-reactivity in $refs, or because complete is a getter, or something else?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests