vue
vue copied to clipboard
$refs should be reactive to be able to use them in computed properties
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?
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')
Because you can have multiple of the same type of fields. Maybe I could use their ID's in that case instead?
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.
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.
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?
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.
Would it be ok to rename the issue to something like: "$refs should be reactive to be able to use them in computed properties"?
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 ;)
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.
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.
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.
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.
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.
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.
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.
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?
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.
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.
Ah good call, technically document.activeElement
is the only thing changing here. Damn. 😞
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.
On second thought that could lead to unnecessary rerenders.
The simplest workaround is using $watch
: https://jsfiddle.net/kenberkeley/9pn1uqam/
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.
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?
<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.
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.
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.
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.
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.
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.