Why is empty object in Vue3 not reactive? - javascript

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.

Related

Different outcomes between Object assign and Object spread

I am using typescript and compiling to ES2016.
I noticed that I am calling two different functions with similar this object in a Function.prototype.call().
I tried to merge both this objects by using a common object which would use ...spread in the beginning of the object like so
let selfShared = {
props,
// ...
};
let selfHost = {
...selfShared,
// ...
};
let selfGuest = {
...selfShared,
// ...
};
The idea of using the spread in the begging was that I could overwrite the shared properties in either of the this objects if I saw it fit.
But unlike when setting props straight in the this objects using the spread gave out weird results, which turned out to be because tsc compiled the code as
let selfShared = {
props
};
let selfHost = Object.assign(Object.assign({}, selfShared), {
// ...
});
// ...
using my code
let state = undefined;
let attributes = {};
let selfShared = {
props: attributes
};
let selfHost = {
...selfHost,
get state() {
console.log("selfHost get state");
return state;
},
set state(originalStates) {
console.log("selfHost set state");
!state ? state = originalStates : console.error("`this.states` already defined in the host function.");
}
}
the output looks like
let state = undefined;
let attributes = {};
let selfShared = {
props: attributes
};
let selfHost = Object.assign(
Object.assign({}, selfShared), {
get state() {
console.log("selfHost get state");
return state;
},
set state(originalStates) {
console.log("selfHost set state");
!state ? state = originalStates : console.error("`this.states` already defined in the host function.");
}
});
now at least on firefox 74 to 77 inserting both of the codes into the console and adding
// ...
selfHost.state = {
thing: "some"
};
selfHost.state = {
some: "thing"
};
throws out different logs...
The precompiled code gives me two of set state and an error which are the expected outputs, but the compiled code gives me a get state and ignores the rule in set state outputting
{
some: "thing"
}
instead of the expected
{
thing: "some"
}
as in the precompiled code?
Setting the spread into the bottom of the file compiles to
let selfHost = Object.assign({
get state() {
console.log("selfHost get state");
return state;
},
set state(originalStates) {
console.log("selfHost set state");
!state ? state = originalStates : console.error("`this.states` already defined in the host function.");
}
}, selfShared);
which gives the right output but doesn't allow me to overwrite the properties given by selfShared.
Can you explain why this happens with Object.assign and if there is a trick to get an output from tsc that still lets me do what I originally wanted?
When using spread
let obj = {
...otherObj,
// ...
}
or
let obj = Object.assign({}, otherObj, {
// ...
})
The spread is interpreted as literal which means that as in the polyfill the properties are read as strict values, which means that normal values are read normally, setters are ignored and getters are read as normal values.
Setters and getters work as written in the question when the spread is written in the end as
let obj = {
// ...
...otherObj
}
or
let obj = Object.assign({
// ...
}, otherObj)
since otherObj only extends the unique objects.

How to use enums (or const) in VueJS?

I feel like an idiot for having to ask about something so seemingly simple, but I'm trying to figure out how to use "enums" in VueJS. Currently, in a file called LandingPage.js I have this bit of code:
const Form = {
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
};
function main() {
new Vue({
el: "#landing-page",
components: {
LoginForm,
WhoIsBehindSection,
WhatIsSection,
Form,
},
data () {
return {
form: Form.LOGIN,
};
},
template: `
<div>
<LoginForm v-if="form === Form.LOGIN"></LoginForm>
<WhatIsSection></WhatIsSection>
<WhoIsBehindSection></WhoIsBehindSection>
</div>
`
});
}
It is the conditional v-if="form === Form.LOGIN" that is failing with the error messages:
Property or method "Form" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Cannot read property 'LOGIN' of undefined
Just so you guys know without the conditional everything is working, and if I were to put this bit in the template
<p>{{ form }}</p>
it will print 0 on the screen. Though, putting this in the template
<p>{{ Form.LOGIN }}</p>
Will not result in it printing 0 on the screen. So I just cannot for the life of me figure out why it will not accept Form.LOGIN.
The Answer
I did add it to components, but never did I think of adding it to data. Happy that it's working now. :)
data () {
return {
form: Form.LOGIN,
Form, // I had to add this bit
};
},
Thank you MarcRo 👍
If you are using Vue in Typescript, then you can use:
import { TernaryStatus } from '../enum/MyEnums';
export default class MyClass extends Vue {
myVariable: TernaryStatus = TernaryStatus.Started;
TernaryStatus: any = TernaryStatus;
}
and then in Template you can just use
<div>Status: {{ myVariable == TernaryStatus.Started ? "Started It" : "Stopped it" }}</div>
You can use https://stackoverflow.com/a/59714524/3706939.
const State = Object.freeze({ Active: 1, Inactive: 2 });
export default {
data() {
return {
State,
state: State.Active
};
},
methods: {
method() {
return state === State.Active;
}
}
}
You only have access to properties of the Vue instance in your template. Just try accessing window or any global in your template, for example.
Hence, you can access {{ form }} but not {{ Form.LOGIN }}.
A wild guess is that it has something to do with how Vue compiles, but I don't know enough about the internals to answer this.
So just keep declaring all the properties you wish to use in your template in your Vue instance (usually as data).
You can enclose enum into class. All your data, the state, the enum variants would be in one place. The same about behaviours, so you will call form.isLogin() rather than form === Form.LOGIN and form.setLogin() rather than form = Form.Login.
The class to generate enums:
class Fenum {
constructor(start, variants) {
this.state = start;
variants.forEach(value => {
const valueC = value.charAt(0).toUpperCase() + value.slice(1);
this['is' + valueC] = () => this.state === value;
this['set' + valueC] = () => this.state = value;
})
}
}
Example of usage:
function main() {
new Vue({
el: "#landing-page",
components: {
LoginForm,
WhoIsBehindSection,
WhatIsSection,
Form,
},
data () {
return {
form: new Fenum("login", ["login", "signUp", "forgotPassword"]),
};
},
template: `
<div>
<LoginForm v-if="form.isLogin()"></LoginForm>
<WhatIsSection></WhatIsSection>
<WhoIsBehindSection></WhoIsBehindSection>
</div>
`
});
}
Vue observe nested objects, so each call of a set method (from.setLogin(), form.setSignUp(), ...) will trigger updates of the component as it should be.
The generated object from this example:
You can use $options instead of $data https://vuejs.org/v2/api/#vm-options
You can use Proxy to create object which throw runtime errors if someone will read non-defined value or try to add new value - here is createEnum (and use it in data() section)
function createEnum(name,obj) {
return new Proxy(obj, {
get(target, property) {
if (property in target) return target[property];
throw new Error(`ENUM: ${name}.${property} is not defined`);
},
set: (target, fieldName, value) => {
throw new Error(`ENUM: adding new member '${fieldName}' to Enum '${name}' is not allowed.`);
}
});
}
// ---------------
// ----- TEST ----
// ---------------
const Form = createEnum('Form',{
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
});
// enum value exists
console.log(Form.LOGIN);
// enum value not exists
try{ console.log(Form.LOGOUT) } catch(e){ console.log(e.message)}
// try to add new value
try{ Form.EXIT = 5 } catch(e){ console.log(e.message)}
for string-like Enums where values are equal to keys you can use following helper
export function createEnumArr(name='', values=[]) {
let obj = {};
values.forEach(v => obj[v]=v);
return createEnum(name,obj);
}
const Form = createEnumArr('Form',[
"LOGIN",
"SIGN_UP",
"FORGOT_PASSWORD",
]);
The easiest way!
in main.js
const enumInfo = {
SOURCE_TYPE: {
WALLET: 1,
QR: 2
}
}
Vue.prototype.enumInfo = enumInfo
index.vue
{{enumInfo}}
For 2022 and beyond you should probably be using Vue 3 and Typescript.
The easiest way to use an enum is to map it to string values and then simply return it from your setup function.
<template>
...
<div v-if="mode == DarkModes.DARK">
do something for dark mode
</div>
...
</template>
<script lang="ts">
enum DarkModes {
BRIGHT = 'bright',
DARK = 'dark',
}
export default defineComponent({
name: 'MyDarkOrBrightComponent',
setup() {
const mode = ref(DarkModes.BRIGHT);
...
return {
mode,
DarkModes, // <- PASS YOUR ENUM HERE!
}
}
});
</script>
And if you're using the new <script setup> functionality it's just as easy ... all top level imports are automatically accessible from the template (if you want to put your enum in a separate file).
I've this problem, too.
Here my solution, just put this in the first line:
<script setup>
const Form = {
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
};
</script>

Javascript, in a React application assign to {} in a function component, code review

I have this code in a friend of mine React application and I need to understand what this code does explicitly:
const Component = ()=> (
<QueryFetcher>
{({ data }) => {
const { user: { profile = {} } = {} } = data
return (
<div>
{profile.username && profile.username}
</div>
)
}}
</QueryFetcher>
)
What is this line for?
const { user: { profile = {} } = {} } = data
Is it correct to assign something to {} using { user: { profile = {} } = {} } in this functional component? Or in a render() hook of a stateful component in React?
const { user: { profile = {} } = {} } = data basically means that your retrieving the user profile.
const means that you are creating a new variable
{ user: { profile } } } means that you are retrieving profile inside of user
= {} means that if the object is undefined, use an empty object so it will not fail because doing user.profile will throw an error if user is undefined.
= data means that you retrieving this info from the data variable
So, this line means, from the variable data, go take the user, if the user is undefined, use an empty object. Then, go take the profile, if the profile is undefined, use an empty object. Then create a variable called profile with the result. This is like doing this:
const user = data.user === undefined ? {} : data.user;
const profile = user.profile === undefined ? {} : user.profile;
What is this line for?
const { user: { profile = {} } = {} } = data
It's basically just chained ES6 object-destructuring with default values.
What this line does in words:
Read "user" from "data", if "user" is undefined, assign {} as a default value
Read "profile" from "user", if "profile" is undefined, assign {} as a default value
Is it correct
It is mostly a short-hand syntax used to remove repetitive stuff. So instead of accessing multiple object props separately e.g.
this.props.prop1, this.props.prop2, ...
you can use
const { prop1, prop2 } = this.props;
It also helps other readers later quickly understanding what variables are used in a method if all necessary props are destructured at the start.

VueJS observe plugged parameter

I'm starting with VueJS 2 and I created a simple plugin which adds parameter to Vue instance.
I have problem because when I update this value my computed properties are still same.
My example plugin's code:
export default function (Vue) {
Vue.MyProperty = "test"
Object.defineProperties(Vue.prototype, {
"$myProperty": {
"get": function () {
return Vue.MyProperty
},
"set": function (value) {
Vue.MyProperty = value
return this
}
}
})
}
And my component's code
export default {
"computed": {
"test": function () {
return this.$myProperty
}
}
}
When I changed this.$myProperty in other component my component returns vaid value (in example when I changed from "test" into "newvalue" I can see "newvalue") but computed property test is still old value ("test" in my example).
I tried to use this.$set(this, "$myProperty", value) but this still not working.
How can I use or declare this property to use it in computed or watched properties?
The reason the data value is not automatically updated in the computed is because the property you added to Vue, MyProperty is not an observed property. Fundamentally, Vue's reactivity works because all values added to data are converted into observed properties; under the hood they are converted into getter/setter pairs with some additional code so that when one of those properties changes, Vue knows to propagate the changes to all the things that depend on it's value.
The code in the question, however, just adds a normal property to the Vue object. You can change it, but it's not reactive.
That said, it's relatively easy to make it reactive. I cover how to do this in the comments to my answer here. Basically, instead of adding your property to Vue, just create a new Vue object (which has very low overhead) and make the property you want to be reactive a property of that Vue. Here is a working example.
console.clear()
function MyPlugin(Vue) {
let store = new Vue({data:{MyProperty: "some value"}})
Object.defineProperties(Vue.prototype, {
"$myProperty": {
"get": function () {
return store.MyProperty
},
"set": function (value) {
store.MyProperty = value
return this
}
}
})
}
Vue.use(MyPlugin)
const MyComponent = {
template:`<div>{{test}}</div>`,
"computed": {
"test": function () {
return this.$myProperty
}
}
}
new Vue({
el: "#app",
components:{
MyComponent
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<my-component></my-component>
<button #click="$myProperty = 'new value'">Change</button>
</div>

Using conditional logic in computed properties fails to update

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.

Categories

Resources