Using conditional logic in computed properties fails to update - javascript

I have two fiddles: A, B (using Vuejs 2.2.4)
I have a computed property which can be changed programmatically (I am using the get and set methods).
Expectations:
If the default parameter changes (this.message), the computed property (computedMessage) must change (default behaviour).
If the secondary parameter changes (this.messageProxy), only then the computed property must reflect the secondary parameter.
Fiddle A works as expected but Fiddle B doesn't.
Error: The default behaviour (point 1) stops after the secondary parameter changes.
The only difference between the fiddles is a console statement in the computed property.
Background: I was trying to set a computed property programatically. The computed property is set like:
computedMessage: {
get () {
let messageProxy = this.messageProxy
this.messageProxy = null
console.log(messageProxy, this.messageProxy, this.message)
return messageProxy || this.message
},
set (val) {
this.messageProxy = val
}
}
This allows me to set the value of computedMessage like:
this.computedMessage = 'some string'
If these lines:
get () {
let messageProxy = this.messageProxy
this.messageProxy = null
return messageProxy || this.message
}
were to be replaced with:
get () {
return this.messageProxy || this.message
}
then computedMessage can no longer get access to this.message the moment this.messageProxy is set.
By setting this.messageProxy to null I ensure that the
computedMessage = this.messageProxy
only if an assignment is made.

The reference to this.message in the return statement isn't triggering computedMessage to update. This is because its location in the logical || statement makes it inaccessible. It's a gotcha documented in the Vue.js Computed Properties Documentation.
From the Docs:
status: function () {
return this.validated
? this.okMsg
: this.errMsg // errMsg isn't accessible; won't trigger updates to status
}
The workaround is to explicitly access dependencies:
status: function () {
// access dependencies explicitly
this.okMsg
this.errMsg
return this.validated
? this.okMsg
: this.errMsg
}
So in your example add a reference to this.message:
get() {
this.message
let messageProxy = this.messageProxy
this.messageProxy = null
return messageProxy || this.message
}
The reason your first fiddle was working as expected was because the console.log call had this.message as a parameter.

The actual problem with your code is that you are changing data values in your get function, and they are data values that trigger the re-computation of the get function. Don't do that. The get should just be computing a value based on other values. In this case, it should be
get () {
console.log(this.messageProxy, this.message);
return this.messageProxy || this.message;
},
With or without the console message, it will do what it is supposed to do.
Having re-checked your expectations, I see that you want the override to be cleared whenever the default message changes. You can do that with an additional watch:
var demo = new Vue({
el: '#demo',
data() {
return {
message: 'I am a great guy',
messageProxy: null,
someText: ''
}
},
computed: {
computedMessage: {
get() {
return this.messageProxy || this.message
},
set(val) {
this.messageProxy = val
}
}
},
methods: {
overrideComputed() {
this.computedMessage = this.someText
}
},
watch: {
message: function() {
this.messageProxy = null;
}
}
})
div {
margin: 5px;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.4/vue.min.js"></script>
<div id="demo">
<p>This message must reflect value of input1</p>
<div>
{{ computedMessage }}
</div>
input1: <input type="text" v-model='message'>
<div>
<p>This will cause computed message to reflect input2</p>
input2: <input type="text" v-model='someText'>
<button #click='overrideComputed'>Override</button>
</div>
</div>
PS: You don't really need a settable computed here. You could have overrideComputed set messageProxy directly.

Related

Why is empty object in Vue3 not reactive?

When I create a ref from an empty object and later add object properties, there is no reactivity:
<template>
<p>{{hello}}</p>
<button #click="add()">Click me</button>
</template>
<script>
import {ref} from 'vue';
export default {
name: "Test",
setup(){
const myData = ref({});
return {myData}
},
methods: {
add(){
this.myData["text"] = "Hello";
console.log(this.myData);
}
},
computed: {
hello(){
return this.myData.hasOwnProperty("text")) ? this.myData["text"] : "no text";
}
}
}
</script>
Clicking the button shows that myData has changed but the computed property hello does not update.
Also tried reactive({}) instead of ref({}) without success.
It works when we initialize the ref with properties, like const myData = ref({"text": "no text"});.
But why does the empty object not work?
EDIT:
Finally found out what exactly the problem is and how it can be solved:
The reactivity core of Vue3 is not alert of Object.keys() but only of the values of the properties, and the empty object does not have any. However, you can make Vue3 alert, if the computed property is defined like
computed: {
hello(){
return Object.keys(this.myData).indexOf("text") > -1 ? this.myData["text"] : "no text";
}
The call to Object.keys(this.myData) is needed to make the reactivity system aware of what we are interested in. This is similar to setting a watch on Object.keys(this.myData) instead of watching this.myData.
Try to you update your ref object like
this.myData = {"text": "Hello"}
const { ref, computed } = Vue
const app = Vue.createApp({
/*setup(){
const myData = ref({});
const hello = computed(() => myData.value.hasOwnProperty("text") ? myData.value.text : myData.value = "no text")
const add = () => {
if(Object.keys(myData.value).length === 0) {
myData.value = {'text': "Hello"};
} else {
myData.value.otherProperty = "Hello again"
}
}
return { myData, add, hello }
},*/
setup(){
const myData = ref({});
return { myData }
},
methods: {
add(){
if(Object.keys(this.myData).length === 0) {
this.myData = {"text": "Hello"}
} else {
this.myData.otherProperty = "Hello again"
}
console.log(this.myData)
},
},
computed: {
hello(){
return Object.keys(this.myData).length !== 0 ? this.myData[Object.keys(this.myData)[Object.keys(this.myData).length - 1]] : "no text"
}
}
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3.2.29/dist/vue.global.prod.js"></script>
<div id="demo">
<p>{{ hello }}</p>
<button #click="add">Click me 2 times</button>
</div>
If you change your computed property to be defined such that it references myData['text'] directly before returning, things work as expected:
computed: {
hello() {
return this.myData['text'] || 'no text'; // works
}
I suspect what's going on with your original code is that the Vue dependency-tracking code is not able to see that your function depends on myData. Consider that hello is being called (by Vue) before the text property exists on the object. In that case, the function returns before actually touching the proxied value (it short-circuits as soon as it sees that hasOwnProperty has returned false).
Dependency tracking in Vue is done dynamically, so if your computed property doesn't touch any reactive variables when called, Vue doesn't see it as having any external dependencies, and so won't bother calling it in the future. It will just use the previously-cached value for subsequent calls.

How to pass object handler into vue component

I want to cache state inside a handler passed by props.
Problem is, the template renders and text content changes well, but the console always throw error:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'apply' of undefined"
Codes below:
<template>
<picker-box :options="item.options" #change="handlePickerChange($event, item)">
<text>{{ item.options[getPickerValue(item)].text }}</text>
</picker-box>
</template>
<script>
export default {
props: {
item: {
type: Object,
default() {
return {
options: [{ text: 'op1' }, { text: 'op2' }],
handler: {
current: 0,
get() {
return this.current
},
set(value) {
this.current = value
},
},
/* I also tried this: */
// new (function () {
// this.current = 0
// this.get = () => {
// return this.current
// }
// this.set = (value) => {
// this.current = value
// }
// })(),
}
},
},
},
methods: {
handlePickerChange(value, item) {
item.handler.set(value)
},
getPickerValue(item) {
return item.handler.get()
},
},
}
</script>
I know it's easy using data() or model prop, but I hope to cahce as this handler.current, in other words, I just want to know why this handler object isn't correct (syntax layer), how can I fix it.
What exactly state are you trying pass?
If you need to keep track of a static property, you could use client's localstorage, or a watcher for this.
If you need to keep of more dynamic data, you could pass it to a computed property that keeps track and sets them everytime they change.

Why I shouldn't set data from computed?

Using vuex, I receive an object, lets suppose
user: {name: 'test'}
And in app.vue I use this.$store.getters.user
computed: {
user: function() {
let user = this.$store.getters.user
return user
}
}
While setting also data object 'this.name'
data() {
return {
name: ''
}
}
computed: {
user: function() {
let user = this.$store.getters.user
this.name = user.name
return user
}
}
But in the lint I get this error 'unexpected side effect in computed property', (the data 'name' should be used as a v-model, to be used as a update API parameter).
I know it can be ignored if you know what you're doing, and that it is triggered for setting data from computed, but why it triggers this error? and how to workaround it?
don't set value in computed. if you need to get name of computed user you must be create new computed:
user: function() {
let user = this.$store.getters.user
return user
},
name: function() {
if(this.user.name!=undefined) return this.user.name
return ''
},
and remove name from data
but if you realy need to set name you can watch user and set name
watch: {
user(newVal) {
if(newVal.name!=undefined) this.name = newVal.name
}
}
Vue has both computed getters and setters. If you define a computed property as you did above it is only a getter. A getter is meant to only "get" a value and it should be a "pure" function with no side effects so that it is easier to read, debug and test.
From the docs for the rule that triggered the linting error on your code:
It is considered a very bad practice to introduce side effects inside
computed properties. It makes the code not predictable and hard to
understand.
In your case you might want to use a computed setter for either the user or the name values so that you can use them as v-models. You could, for example, do:
computed: {
user: function () {
return this.$store.getters.user;
},
user: {
// getter
get: function () {
return this.user.name;
},
// setter
set: function (newValue) {
this.$store.commit('setUserName', newValue);
}
}
}

Issue in Typing a class with Flowjs

I have the following code that I'm attempting to type with Flow
type Metadata = {
currentPage: string,
};
type State = {
result: {
metadata: Metadata,
}
}
type EmptyObject = {};
type Test = Metadata | EmptyObject;
class HelperFn {
state: State;
metadata: Test;
constructor(state: State) {
this.state = state;
if (state && state.result && state.result.metadata) {
this.metadata = state.result.metadata;
} else {
this.metadata = {};
}
}
getCurrentPageNumber() {
return this.metadata.currentPage;
}
}
I've created Types that I'm assigning later on. In my class, I assign the type Test to metadata. Metadata can be either an object with properties or an empty object. When declaring the function getCurrentPageNumber, the linter Flow tells me that it
cannot get 'this.metadata.currentPage' because property 'currentPage' is missing in EmptyObject
Looks like Flow only refers to the emptyObject. What is the correct syntax to tell Flow that my object can either be with properties or just empty?
Since metaData can be empty, Flow is correctly telling you that this.metadata.currentPage may not exist. You could wrap it in some sort of check like
if (this.metadata.currentPage) {
return this.metadata.currentPage
} else {
return 0;
}
To get it to work properly.

How to avoid duplication between mounting and updating a Vue component

In a Vue application I'm working on I have a number of form components that can be used to either create a new record or amend an existing one. While a form is open, it is also possible to click another record or click create, in which case the contents of the form are replaced or cleared respectively.
The problem I have is that I can't seem to avoid a lot of duplication between my data function and my watch functions.
Here's a simplified example of the kind of thing I mean:
props: ["record"],
data() {
return {
name: this.record ? this.record.name : "",
age: this.record ? this.record.age : null
};
},
watch: {
record(record) {
this.name = record ? record.name : "";
this.age = record ? record.age : null;
}
}
Everything I have to do to set up the form when it's mounted has to be done twice: once in the data function to set up the initial reactive properties, and then again in watch for any props that could change. This gets more and more difficult to manage and mistake-prone as the number of properties in the record gets bigger.
Is there any way to keep this setup logic in one place and avoid this duplication?
To resolve the problem add a immediate property to your watcher which will make it call at initialization too. Therefore the initial value of your record property will be handled. Take a look at the code below:
props: ["record"],
data() {
return {
name: "",
age: null
};
},
watch: {
record: {
immediate: true,
handler(value) {
this.name = this.record ? this.record.name : "";
this.age = this.record ? this.record.age : null;
}
}
}
Reference: vm.$watch - Vue's Official API
How about this?
props: ["record"],
data() {
return this.updateRecord(this.record, {});
},
watch: {
record(record) {
this.updateRecord(record, this);
}
},
updateRecord(what, where) {
where.name = what ? what.name : "";
where.age = what ? what.age : null;
return where;
}

Categories

Resources