Vuex : state change not updating input field - javascript

**** UPDATE ****
Solution : Not bothering to tie the Vuex state to the form. I realized it's not worth the trouble, and I can still serialize the form data and send it along anyway using the standard conventions. Had a total rethinking of not needing to tie so many elements into JS objects.
**** ORIG ****
Alright, can someone please tell me what I'm missing here.
I want to update the state in a mutation function, and have the changes reflect in the input fields. Crazy right? I guess there's trouble with updating object properties, so in the example I have both referencing the object property and defining a specific getter for the property, neither of which are working. Then to represent updating the state outside of the input field, I just have a button below that does this.
Template:
<input :value="user.first_name" #input="UPDATE_USER" />
OR
<input :value="first_name" #input="UPDATE_USER" />
<button v-on:click="updateName">Update</button>
Component:
computed: {
...mapState({
user: state => state.user,
first_name: state => state.user.first_name // specific getter for the property
})
},
methods: {
...mapActions([
UPDATE_USER,
]),
updateName () {
this.$store.dispatch(UPDATE_USER_NAME, "anything");
}
}
Action:
[UPDATE_USER_NAME] ({commit}, name) {
commit("updateUserName", name);
},
// Omitted UPDATE_USER function, just takes e.target.name / value and it works anyway
Mutations:
updateUserName (state, name) {
Vue.set(state.user, "first_name", name);
}
Expected: Click the button, it updates the state, and the input field value shows "anything".
Actual: Click the button, it updates the state, but the input field value is not updated. This goes for both input fields, one which references the object property directly, and the other which has its own getter.
Editing the input fields works fine, so it's like it works top-down but not bottom-up. What the heck am I missing?

Note: I tested this locally but I don't know exactly how the store state looks from the question.
The <input> can be two-way bound with v-model, instead of computed or watched. It can be initialized with a value from the store (I used the mounted lifecycle hook below). The store state is updated only on button click.
<input v-model="firstName"/>
<button #click="updateName">Update</button>
import { mapActions, mapGetters } from 'vuex'
export default {
data () {
return {
firstName: ''
}
},
computed: mapGetters(['getFirstName']),
methods: {
...mapActions(['UPDATE_FIRST_NAME']), // get the action from the store
updateName () {
// validations can go here etc
this.UPDATE_FIRST_NAME(this.firstName) // update vuex store state
}
},
mounted () {
this.firstName = this.getFirstName // initialize this.firstName -> <input>
}
}
With this solution, you'd have to make sure to create the getter in your store, as in the following example:
const state = {
user: {
firstName: ''
}
}
const getters = {
getFirstName: state => state.user.firstName
}

You need to watch the state variables. When it change the value, the the watch function will fire.
export default {
data() {
return {
dummy_name: '',
first_name: '',
}
},
created(){
},
computed: {
dummy_name() {
return this.$store.state.user.first_name
},
watch: {
dummy_name() {
this.first_name = this.dummy_name
}
}
Hope this will help, and get some idea how watch and computed work.

This is an old question, but I ran into the same problem yesterday and the solution was to use the .prop modifier on the input value attribute like so:
<input :value.prop="first_name" #change="updateName" />
A quote from quite random place in the docs says:
For some properties such as value to work as you would expect, you will need to bind them using the .prop modifier.
Hope this helps someone!

Related

How to get the props value and use in lifehooks cycle of Vue JS

I'm searching a way to get the props value through some lifehooks like mounted or updated and trying to save the value with my v-model with some string. But I can't get it.
Though I tried :value on the input element with the props value and some string and I was able to get it, but it seems like I can't access it without v-model, as I researched v-model and :value can't be together.
The purpose is to get the value(with from props and some string) of a input tags.
Parent Component
<invite :user_token="user_token"/>
Child Component
export default {
props: ['user_token'],
data() {
return {
link: ''
}
},
mounted() {
console.log(this.user_token);
this.link = `"http://localhost/johndoe-vue/public/#/invite${this.user_token}"`;
},
updated() {
console.log(this.user_token);
this.link = `"http://localhost/johndoe-vue/public/#/invite${this.user_token}"`;
}
}
Welcome to SO Nigel!
Are you looking for something like this, perhaps?
ParentComponent.vue
<template>
<div id="wrapper">
<invite :userToken="userToken"></invite>
</div>
</div>
<script>
import Invite from "#/Invite.vue";
export default {
components: {
Invite
},
data() {
return {
userToken: "fooBar",
};
}
}
</script>
ChildComponent.vue
<template>
<div id="wrapper">
<p v-if="inviteLink != ''">{{ inviteLink }}</p>
</div>
</template>
export default {
props: {
userToken: {
type: String,
}
},
data() {
return {
inviteLink: ""
}
},
created() {
if(this.userToken != "") {
this.inviteLink == "/your-link-here/"+this.userToken;
}
}
}
Also, you should check out the Vue.js Style Guide. They've marked multi-word component names as essential. Your Invite component should be renamed to BaseInvite or something like that.
Have you tried to $emit this.link
Props is accessible through the $props property of your component. You would reference it like: this.$props.[property name]. $props is called an instance property; there are many of them and they are each accessible this way. See https://v2.vuejs.org/v2/api/#Instance-Properties
Keep in mind that the Vue life cycle methods are somewhat inconsistent. Which instance properties are accessible depends on the method (ie: you can't reference $el in created(...).
https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram

Vue2: warning: Avoid mutating a prop directly

I'm stuck in the situation where my child component (autocomplete) needs to update a value of its parent (Curve), And the parent needs to update the one of the child (or to completely re-render when a new Curve component is used)
In my app the user can select a Curve in a list of Curve components. My previous code worked correctly except the component autocomplete was not updated when the user selected another Curve in the list (the component didn't update its values with the value of the parent).
This problem is fixed now but I get this warning:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"value"
The description of this warning explain exactly what behavior I expect from my components. Despite this warning, this code works perfectly fine !
Here is the code (parts of it have been removed to simplify)
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
computed: {
curve() { return this.value }
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
computed: {
content: {
get() { return this.value },
set(newValue) { this.value = newValue }
}
}
}
</script>
A lot of people are getting the same warning, I tried some solutions I found but I was not able to make them work in my situation (Using events, changing the type of the props of Autocomplete to be an Object, using an other computed value, ...)
Is there a simple solution to solve this problem ? Should I simply ignore this warning ?
you can try is code, follow the prop -> local data -> $emit local data to prop flow in every component and component wrapper.
ps: $emit('input', ...) is update for the value(in props) bind by v-model
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
data() {
return { currentValue: this.value }
}
computed: {
curve() { return this.currentValue }
},
watch: {
'curve.y'(val) {
this.$emit('input', this.currentValue);
}
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
data() {
return { currentValue: this.value };
},
computed: {
content: {
get() { return this.value },
set(newValue) {
this.currentValue = newValue;
this.$emit('input', this.currentValue);
}
}
}
}
</script>
You can ignore it and everything will work just fine, but it's a bad practice, that's what vue is telling you. It'll be much harder to debug code, when you're not following the single responsibility principle.
Vue suggests you, that only the component who owns the data should be able to modify it.
Not sure why events solution ($emit) does not work in your situation, it throws errors or what?
To get rid of this warning you also can use .sync modifier:
https://v2.vuejs.org/v2/guide/components.html#sync-Modifier

Error in render function: “TypeError: Cannot read property of undefined”, using Vuex and Vue Router

I have been dealing with an issue using Vue, Vuex and Vue-Router. I'm building a flash cards app, fetching all the cards on main app creation, then using a Vuex getter to get each card by its id which is passed as a route parameter.
Relevant bits:
App.vue
export default {
components: {
'app-header': header,
},
data() {
return {
}
},
created() {
this.$store.dispatch('getAllCards');
}
}
The dispatch('getAllCards') is just pulling all the cards from the DB and committing to Vuex store.js.
Now I set up a getter:
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card._id === id);
}
}
Here is Card.vue:
<template>
<div>
<br>
<div v-if="flipped" class="container">
<div class="box">
<pre v-if="card.code"><code class="preserve-ws">{{card.back}}</code></pre>
<p class="preserve-ws center-vertical" v-else>{{card.back}}</p>
</div>
</div>
<div v-else class="container">
<div class="box">
<h1 class="title has-text-centered center-vertical is-2">{{card.front}}</h1>
</div>
</div>
</template>
<script>
export default {
data() {
return {
card: {},
flipped: false,
general_card: false,
code_card: true,
random_card: false
}
},
computed: {
},
methods: {
},
created() {
this.card = this.$store.getters.cardById(this.$route.params.id);
}
}
</script>
I am getting the TypeError referenced in the title. My understanding is that the created() hook happens after data() has been set up, so then I can assign {card} using the getter. Unfortunately this displays nothing...
If I assign card() as a computed property:
computed: {
card() {
return this.$store.getters.cardById(this.$route.params.id);
}
}
The card shows, but I still get that error in console. Any idea why? I looked at this and attempted that solution, but to no avail.
The question don't have all the premise to get a correct answer.
(We don't know the mutations, the state, neither the actions and we have no clues about app-header component)
So we have to admit some hypothesis :
state.allCards is an empty Array when you mount the component
action getAllCards is an async function to retrieve or set state.allCards with a mutation
cardById's getters, return a function, so vuex can't apply reactive on a function. So if the state change, the getter won't be trigger correctly.
To correct this, use computed as you mention here.
But it don't fix the undefined error, that from my point of view, is because the getters return undefined on mount.
getters: {
cardById: (state) => (id) => {
var card = state.allCards.find((card) => card._id === id);
if(card) {
return card;
}
// on undefined return a default value
return {
front:'default value'
};
}
}
You can see the implementation on this jsfiddle with no error on console.
Or you can have a loading state on undefined value for your card component.
If my hypothesis is wrong please provide a jsfiddle to help you.
What you need is a derived state based on store state which is to return a filtered card based on a card id. This id is received to your component via the route params. So its better you use a computed property instead of passing arguments to the store getters
Instead of initializing card in data property make card a computed property like this:
computed:{
card(){
return this.$store.state.allCards.find((card) => card._id === this.$route.params.id);
}
}
Note this
If a component needs derived store state based on its own state(in your case rourte params), it should define a local computed property
I tried everyone else's solutions and they did not work. But I got it to work finally. Here is what worked for me:
I ended up including a top-level:
<div v-if="!card"> Loading card... </div>
<div v-else> Rest of card template </div>
That seems to have silenced the error. Also, the card lives as a computed property:
card() {
return this.$store.getters.cardById(this.$route.params.id);
}
I think it was throw error in this step
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card._id === id);
}
}
in this step, it can not find _id of cars, because allcards was null;
and then you use computed instead, when allcards has been change, it will get again;
you can change code like this
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card && card._id === id);
}
}
$route.params.id must be string, so what is the type of card._id? It seems each card._id is number, I think.

React state lifecycle for a filter component

I'm just looking for advice on how to properly set / read state in a component that is just a filter (i.e. select dates, min max values, etc).
I basically have:
onMinDateChange(minDate) {
this.setState({minDate});
},
onMaxDateChange(maxDate) {
this.setState({maxDate});
},
...
Now I want to call this.props.onChange() on every state change, but I have two issues:
state doesn't immediately update; how do I call this on the "next tick"? componentDidUpdate?
I'm not sure how to observe any state change so that I don't have to write:
onMinDateChange(minDate) {
this.setState({minDate});
this.update();
},
onMaxDateChange(maxDate) {
this.setState({maxDate});
this.update();
},
...
Any help on both of these points?
You can pass a callback to the this.setState(). see below:
_onStateUpdate() {
*Some code here*
}
onMinDateChange(minDate) {
this.setState({minDate}, _onStateUpdate);
},
Regarding the both of your issues, including this one:
I'm not sure how to observe any state change
You can use componentDidUpdate( prevProps, prevState ) callback, and determine inside, whenever the state was changed.
https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
Here you're trying to synchronize your local state with upper components. That's possible, but it's a hard thing to do it right. Especially when you will occasionally need to set some filter values from the top. Consider moving the state of the filter to the upper component.
To do that you will need to pass your state object and function used to update it instead of setState as component props. State object would be stored as a part of the upper component state then.
You can use value links to make it look elegant. Here's what you code will look like in this case:
import { Link } from 'valuelink'
import { Input } from 'valuelink/tags.jsx'
const UpperComponent = React.createClass({
getInitialState(){
return {
filter : {
text : '',
...
}
}
},
render(){
return (
...
<Filter state={ Link.state( this, 'filter' ) } />
)
}
});
const Filter = ({ state }) => (
<div className="filter">
<Input valueLink={ state.at( 'text' ) } />
...
</div>
)
Here's an explanation of the technique:
https://medium.com/#gaperton/state-and-forms-in-react-part-3-handling-the-complex-state-acf369244d37#.nuloz9adx
And here's the lib:
https://github.com/Volicon/NestedLink

Can you force Vue.js to reload/re-render?

Just a quick question.
Can you force Vue.js to reload/recalculate everything? If so, how?
Try this magic spell:
vm.$forceUpdate();
//or in file components
this.$forceUpdate();
No need to create any hanging vars :)
Update: I found this solution when I only started working with VueJS. However further exploration proved this approach as a crutch. As far as I recall, in a while I got rid of it simply putting all the properties that failed to refresh automatically (mostly nested ones) into computed properties.
More info here: https://v2.vuejs.org/v2/guide/computed.html
This seems like a pretty clean solution from matthiasg on this issue:
you can also use :key="someVariableUnderYourControl" and change the key when you want to component to be completely rebuilt
For my use case, I was feeding a Vuex getter into a component as a prop. Somehow Vuex would fetch the data but the reactivity wouldn't reliably kick in to rerender the component. In my case, setting the component key to some attribute on the prop guaranteed a refresh when the getters (and the attribute) finally resolved.
Please read this
http://michaelnthiessen.com/force-re-render/
The horrible way: reloading the entire page The terrible way:
using the v-if hack The better way: using Vue’s built-in
forceUpdate method The best way: key-changing on your
component
<template>
<component-to-re-render :key="componentKey" />
</template>
<script>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
</script>
I also use watch: in some situations.
Try to use this.$router.go(0); to manually reload the current page.
Why?
...do you need to force an update?
Perhaps you are not exploring Vue at its best:
To have Vue automatically react to value changes, the objects must be initially declared in data. Or, if not, they must be added using Vue.set().
See comments in the demo below. Or open the same demo in a JSFiddle here.
new Vue({
el: '#app',
data: {
person: {
name: 'Edson'
}
},
methods: {
changeName() {
// because name is declared in data, whenever it
// changes, Vue automatically updates
this.person.name = 'Arantes';
},
changeNickname() {
// because nickname is NOT declared in data, when it
// changes, Vue will NOT automatically update
this.person.nickname = 'Pele';
// although if anything else updates, this change will be seen
},
changeNicknameProperly() {
// when some property is NOT INITIALLY declared in data, the correct way
// to add it is using Vue.set or this.$set
Vue.set(this.person, 'address', '123th avenue.');
// subsequent changes can be done directly now and it will auto update
this.person.address = '345th avenue.';
}
}
})
/* CSS just for the demo, it is not necessary at all! */
span:nth-of-type(1),button:nth-of-type(1) { color: blue; }
span:nth-of-type(2),button:nth-of-type(2) { color: red; }
span:nth-of-type(3),button:nth-of-type(3) { color: green; }
span { font-family: monospace }
<script src="https://unpkg.com/vue"></script>
<div id="app">
<span>person.name: {{ person.name }}</span><br>
<span>person.nickname: {{ person.nickname }}</span><br>
<span>person.address: {{ person.address }}</span><br>
<br>
<button #click="changeName">this.person.name = 'Arantes'; (will auto update because `name` was in `data`)</button><br>
<button #click="changeNickname">this.person.nickname = 'Pele'; (will NOT auto update because `nickname` was not in `data`)</button><br>
<button #click="changeNicknameProperly">Vue.set(this.person, 'address', '99th st.'); (WILL auto update even though `address` was not in `data`)</button>
<br>
<br>
For more info, read the comments in the code. Or check the docs on <b>Reactivity</b> (link below).
</div>
To master this part of Vue, check the Official Docs on Reactivity - Change Detection Caveats. It is a must read!
Use vm.$set('varName', value).
Look for details into Change_Detection_Caveats
Sure .. you can simply use the key attribute to force re-render (recreation) at any time.
<mycomponent :key="somevalueunderyourcontrol"></mycomponent>
See https://jsfiddle.net/mgoetzke/epqy1xgf/ for an example
It was also discussed here: https://github.com/vuejs/Discussion/issues/356#issuecomment-336060875
<my-component :key="uniqueKey" />
along with it use this.$set(obj,'obj_key',value)
and update uniqueKey for every update in object (obj) value
for every update this.uniqueKey++
it worked for me this way
So there's two way you can do this,
You can use $forceUpdate() inside your method handler i.e
<your-component #click="reRender()"></your-component>
<script>
export default {
methods: {
reRender(){
this.$forceUpdate()
}
}
}
</script>
You can give a :key attribute to your component and increment when want to rerender
<your-component :key="index" #click="reRender()"></your-component>
<script>
export default {
data() {
return {
index: 1
}
},
methods: {
reRender(){
this.index++
}
}
}
</script>
In order to reload/re-render/refresh component, stop the long codings. There is a Vue.JS way of doing that.
Just use :key attribute.
For example:
<my-component :key="unique" />
I am using that one in BS Vue Table Slot. Telling that I will do something for this component so make it unique.
Using v-if directive
<div v-if="trulyvalue">
<component-here />
</div>
So simply by changing the value of trulyvalue from false to true will cause the component between the div to rerender again
Dec, 2021 Update:
You can force-reload components by adding :key="$route.fullPath".
For Child Component:
<Child :key="$route.fullPath" />
For router-view tag:
<router-view :key="$route.fullPath" />
However, :key="$route.fullPath" only can force-reload the components of the different route but not the components of the same route. To be able to force-reload the components of the same route as well, we need to add "value" with an array to :key="$route.fullPath" and change "value". So it becomes :key="[$route.fullPath, value]" and we need to change "value".
*We can assign Array to :key=.
<template>
<Child
:key="[$route.fullPath, value]" // Can assign "Array" to ":key="
#childReload="reload" // Call #click="$emit('childReload')" in
/> // Child Component to increment the value.
</template>
OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR
<template>
<router-view
:key="[$route.fullPath, value]" // Can assign "Array" to ":key="
#routerViewReload="reload" // Call #click="$emit('routerViewReload')"
/> // in Child Component to increment the value.
</template>
<script>
export default {
name: "Parent", components: { Child, },
data() {
return {
value: 0,
};
},
methods: {
reload() {
this.value++;
}
}
}
</script>
However, to keep using both "$route.fullPath" and "value" causes some error sometimes so only when some event like Click happens, we use both "$route.fullPath" and "value". Except when some event like Click happens, we always need to use only "$route.fullPath".
This is the final code:
<template>
<Child
:key="state ? $route.fullPath : [$route.fullPath, value]"
#childReload="reload" // Call #click="$emit('childReload')" in
/> // Child Component to increment the value.
</template>
OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR
<template>
<router-view
:key="state ? $route.fullPath : [$route.fullPath, value]"
#routerViewReload="reload" // Call #click="$emit('routerViewReload')" in
/> // Child Component to increment the value.
</template>
<script>
export default {
name: "Parent", components: { Child, },
data() {
return {
state: true,
value: 0,
};
},
methods: {
reload() {
this.state = false;
this.value++;
this.$nextTick(() => this.state = true);
}
}
}
</script>
Unfortunately, there are no simple ways to force-reload components properly in Vue. That's the problem of Vue for now.
This has worked for me.
created() {
EventBus.$on('refresh-stores-list', () => {
this.$forceUpdate();
});
},
The other component fires the refresh-stores-list event will cause the current component to rerender
<router-view :key="$route.params.slug" />
Just use key with your any params its auto reload children..
I found a way. It's a bit hacky but works.
vm.$set("x",0);
vm.$delete("x");
Where vm is your view-model object, and x is a non-existent variable.
Vue.js will complain about this in the console log but it does trigger a refresh for all data. Tested with version 1.0.26.
Worked for me
data () {
return {
userInfo: null,
offers: null
}
},
watch: {
'$route'() {
this.userInfo = null
this.offers = null
this.loadUserInfo()
this.getUserOffers()
}
}
The approach of adding :key to the vue-router lib's router-view component cause's fickers for me, so I went vue-router's 'in-component guard' to intercept updates and refresh the entire page accordingly when there's an update of the path on the same route (as $router.go, $router.push, $router.replace weren't any help). The only caveat with this is that we're for a second breaking the singe-page app behavior, by refreshing the page.
beforeRouteUpdate(to, from, next) {
if (to.path !== from.path) {
window.location = to.path;
}
},
Except page reload method(flickering), none of them works for me (:key didn't worked).
and I found this method from old vue.js forum which is works for me:
https://github.com/vuejs/Discussion/issues/356
<template>
<div v-if="show">
<button #click="rerender">re-render</button>
</div>
</template>
<script>
export default {
data(){
return {show:true}
},
methods:{
rerender(){
this.show = false
this.$nextTick(() => {
this.show = true
console.log('re-render start')
this.$nextTick(() => {
console.log('re-render end')
})
})
}
}
}
</script>
Add this code:
this.$forceUpdate()
For anyone still looking around, there's a package for this now.
https://github.com/gabrielmbmb/vuex-multi-tab-state
All I had to do was install it and add it to my plugins in main.ts (as it shows on that page) and it did exactly what I wanted.
If your URL changes as well when if the component is loaded you can just use it in the :key attribute. This works especially well if you use it on the router-view tag directly. And this commes with the added benedit of the key being a value that is actually tied to the content of the page instead of just some random number.
<router-view :key="this.$route.path"></router-view>
If you are using router-view or Vue Router, you can directly use the key feature
<router-view :key="$route.path"></router-view>
This will tell the router view to re-render the page every time the path is changed.

Categories

Resources