This code is from a VueMastery course, which must be outdated:
export default {
setup(props, {emit}){
let email = props.email;
let toggleRead = () => {
email.read = !email.read
axios.put(`http://localhost:3000/emails/${email.id}`, email)
}
...
It gives this error:
71:9 error Getting a value from the `props` in root scope of `setup()` will cause the value to lose reactivity vue/no-setup-props-destructure
Note that I am not dealing with const here. What is the correct way to make a prop value reactive in Vue 3?
This warning is caused by linter rule that is supposed to improve code quality. If it's known that a prop isn't changed during the lifetime of component instance, it can be disabled.
The problem here is that the code mutates a prop, which is considered a bad practice and can trigger another warning.
For one-way change of prop value, i.e. a parent is unaware of it:
const toggleRead = () => {
const email = { ...props.email, read: !email.read };
axios.put(`http://localhost:3000/emails/${email.id}`, email)
}
For two-way change:
const toggleRead = () => {
...
emit('emailUpdate', email);
}
A parent should listen to emailUpdate event and update its state accordingly.
Related
I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".
We are trying to detect whether a person is logged in or not using the vuex store state: loggedIn. When I call the API service from the action it calls the mutation after successful login and changes the data in the state:
loginSuccess(state, accessToken) {
state.accessToken = accessToken;
state.authenticating = false;
state.loggedIn = true;
console.log(state.loggedIn);
}
The console.log() shows the value, so the mutation is working.
In my other component, I use a computed property to watch for changes in the store using ...mapState() and bound the property in the template view:
computed: {
...mapState('authStore',['loggedIn' ]);
}
But the view never gets updated based on the computed property. I checked using the Vue dev tools in the console. It shows the state changes.
I have initialized the state.
export const states = {
loggedIn: false
};
I have tried to call the state directly.
this.$store.state.authStore.loggedIn;
I have tried different approaches.
...mapState('authStore', { logging:'loggedIn' });
//or
...mapState('authStore',['loggedIn' ]);
also, tried watch: {} hook but not working.
Interestingly though, the state's getter always shows undefined, but the state property changes in the dev tools.
Cannot figure out what is wrong or how to move further.
here is the screenshot of devtools state after successful login:
This catches my eye:
export const states = {
loggedIn: false
};
My suspicion is that you're then trying to use it something like this:
const store = {
states,
mutations,
actions,
getters
}
This won't work because it needs to be called state and not states. The result will be that loggedIn is unreactive and has an initial value of undefined. Any computed properties, including the store's getter, will not be refreshed when the value changes.
Whether my theory is right or not, I suggest adding console.log(state.loggedIn); to the beginning of loginSucess to confirm the state prior to the mutation.
Is there a way to update property the same way as Vue.delete is used?
I have this method:
let element = event.target
let url = '/admin/services/changePriorityStatus/' + element.dataset.id
this.$dialog.confirm('Are you sure you want to change priority status of this service?',{
loader: true
}).then((dialog) => {
axios.post(url, {
id: element.dataset.id,
priority: element.dataset.priority
})
.then((response) => {
let data = response.data
dialog.close()
let serviceIndex = this.services.findIndex(service => serviceID)
Vue.delete(this.services, serviceIndex);
Event.$emit('message:show', {
messageTitle: data.messageTitle,
messageText: data.messageText
}, data.class)
})
.catch((error) => {
alert('Something went wrong. Please try again a bit later')
dialog.close()
})
})
.catch(() => {
console.log('Changing status canceled')
});
I want to replace Vue.delete with some global update method if it is possible. Is there a way to do this?
Yes with Vue.set. However, it's only needed when adding a new property to an object, not when updating an already reactive property.
See https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats for more information:
Change Detection Caveats
Due to the limitations of modern JavaScript (and the abandonment of
Object.observe), Vue cannot detect property addition or deletion.
Since Vue performs the getter/setter conversion process during
instance initialization, a property must be present in the data object
in order for Vue to convert it and make it reactive.
Vue does not allow dynamically adding new root-level reactive
properties to an already created instance. However, it’s possible to
add reactive properties to a nested object using the Vue.set(object, key, value) method.
I know that I am supposed to use mutations to change state. However I was wondering if it is theoretivally possible to use state in a v-model binding.
My current solution:
html:
...
<input v-model='todo'>
...
with mutation:
...
computed: {
todo: {
get () { return this.$store.state.todos.todo },
set (value) { this.$store.commit('updateTodo', value) }
}
}
...
without mutation
...
computed: {
todo: {
get () { return this.$store.state.todos.todo },
set (value) { this.$store.state.todos.todo = value }
}
}
...
what I would like:
...
<input v-model='this.$store.state.todos.todo'>
...
You can directly bind a Vuex state property to a component or input via v-model:
<input v-model='$store.state.todos.todo'>
But this is strongly recommended against. Vuex will warn you that you are mutating the state outside of a mutation function.
Since, when using Vuex, your state object is your source of truth which is designed to only be updated in a mutation function, it will quickly become hard to debug why the global state is changing if one component is affecting the global state without calling a mutation.
Most people, I believe, would recommend using your computed todo property example with mutations for the scenario you're describing.
I have a Vuex store with the following state:
state: {
authed: false,
id: false
}
Inside a component I want to watch for changes to the authed state and send an AJAX call to the server. It needs to be done in various components.
I tried using store.watch(), but that fires when either id or authed changes. I also noticed, it's different from vm.$watch in that you can't specify a property. When i tried to do this:
store.watch('authed', function(newValue, oldValue){
//some code
});
I got this error:
[vuex] store.watch only accepts a function.
Any help is appreciated!
Just set a getter for the authed state in your component and watch that local getter:
watch: {
'authed': function () {
...
}
}
Or you can use ...
let suscribe = store.subscribe((mutation, state) => {
console.log(mutation.type)
console.log(mutation.payload)
})
// call suscribe() for unsuscribe
https://vuex.vuejs.org/api/#subscribe