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 · 42 comments

Comments

Projects
None yet
@TheDutchCoder

TheDutchCoder commented Oct 3, 2016

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

This comment has been minimized.

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')

@LinusBorg LinusBorg added the discussion label Oct 4, 2016

@TheDutchCoder

This comment has been minimized.

TheDutchCoder commented Oct 4, 2016

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

@kingdaro

This comment has been minimized.

Contributor

kingdaro commented Oct 4, 2016

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

This comment has been minimized.

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

This comment has been minimized.

Member

LinusBorg commented Oct 4, 2016

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

This comment has been minimized.

TheDutchCoder commented Oct 4, 2016

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

This comment has been minimized.

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

This comment has been minimized.

TheDutchCoder commented Oct 4, 2016

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 from We need some replacement for refs to $refs should be reactive to be able to use them in computed properties Oct 4, 2016

@kingdaro

This comment has been minimized.

Contributor

kingdaro 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

This comment has been minimized.

TheDutchCoder commented Oct 4, 2016

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

This comment has been minimized.

TheDutchCoder commented Oct 7, 2016

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

This comment has been minimized.

pdemello commented Oct 11, 2016

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

This comment has been minimized.

Member

blake-newman commented Oct 13, 2016

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

This comment has been minimized.

paulpflug commented Oct 14, 2016

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

This comment has been minimized.

gustogummi commented Jan 1, 2017

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

This comment has been minimized.

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?

@docmars

This comment has been minimized.

docmars 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

This comment has been minimized.

Member

LinusBorg commented Apr 20, 2017

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.

@docmars

This comment has been minimized.

docmars commented Apr 20, 2017

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

@LinusBorg

This comment has been minimized.

Member

LinusBorg commented Apr 20, 2017

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

This comment has been minimized.

Member

LinusBorg commented Apr 20, 2017

On second thought that could lead to unnecessary rerenders.

@kenberkeley

This comment has been minimized.

kenberkeley commented Jul 10, 2017

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

@tomwang1013

This comment has been minimized.

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

This comment has been minimized.

AndreKR commented Oct 13, 2017

Related: https://forum.vuejs.org/t/split-modal-and-its-contents-slots/18338/6

As I'm thinking about it, it seems to me that if $refs isn't reactive, it's completely useless outside event handlers, isn't it?

@bbsimonbb

This comment has been minimized.

bbsimonbb commented Oct 19, 2017

<v-disclaimer>not a guru</v-disclaimer>
An awful lotta folk seem to get into trouble because they don't think view-model. Vue is "loosely inspired by MVVM". You should at least be at step 1 of the store pattern: a global variable that contains all your page state. To take your example, email and password, their current values, and, I would say, their validity, are page level state. Your inputs should be interacting directly with the state (declared in their data if they're components). And your computed prop, isValid, also just looks at the state variable. You can implement all this without ref, and if you can do without ref, you should.

I see so many folk getting into knots because they don't think about their store. Having Vue stuff directly talking to other vue stuff is an anti-pattern. Vue wants to interact with a store.

@drzraf

This comment has been minimized.

drzraf commented Oct 19, 2017

But when an application starts to have multiple controls and various components, then you start have multiple stores (component's store) in order to organize the state of the application into logical reactive objects bound to their respective component.
Then you need to access them from the main Vue, and then $refs reactivity start to be useful because otherwise you start using signal for top-down and signal's mess is not far away.

@maxnorth

This comment has been minimized.

maxnorth commented Oct 27, 2017

Personally, my impression of $refs was that it is mostly meant to be useful as a means to get an html element reference when you absolutely need it, and these aren't reactive anyway. Using it as a way to directly access a child component's state seems like a violation of good component design. I agree that it would be convenient, but I could see myself being tempted to take advantage of that convenience in ways that would ultimately make the components harder to manage. Especially with a third party component - the component creator only makes guarantees about its props/events interface. It's ultimately no one's business how it uses its internal state, and we shouldn't make any assumptions about it remaining compatible between versions, nor should the author have to worry about others relying on its use of internal state.

@AndreKR

This comment has been minimized.

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

This comment has been minimized.

maxnorth commented Oct 28, 2017

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

This comment has been minimized.

maxnorth commented Oct 28, 2017

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.

@BlitZz96

This comment has been minimized.

BlitZz96 commented Jan 9, 2018

Any updates on this?

@Akryum

This comment has been minimized.

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

This comment has been minimized.

@Akryum

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

trusktr commented May 8, 2018

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

@kryvonos-v

This comment has been minimized.

kryvonos-v commented Aug 8, 2018

Is there any progress with this feature?

@anthonygore

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Contributor

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

This comment has been minimized.

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

This comment has been minimized.

tmorehouse commented Nov 3, 2018

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.

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