React functions: changing state after useState - react-native

I have a functional component that manages a question that can come from two sources.
A props value comes in indicating the source.
When the source changes, I want to create a new question model object with the new source.
Since I'm doing something like this:
const [questionModel, setQuestionModel ] = useState(new QuestionModel(questionSource));
For some reasons it thinks, "Oh, I've already got one of those questionModel's. I don't need to make a new one".
So all the old stuff stays there.
If I try to do something like:
setQuestionModel(new QuestionModel(questionSource));
Then it complains about:
Invariant Violation: Too many re-renders. React limits the number of
renders to prevent an infinite loop.
I get that infinite loops are bad, but I'm not sure how to make this work in ReactJS functions with hooks.
Back when I was using classes, I could specify something once in the constructor and then adjust it again in the render(). But now that the render is mixed in with the other code for the function, how do I re-render it with the new source?
Is there a force re-render when the props change? I thought it would do that if a prop changed ... but it doesn't.

I don't know how your props changes but I saw sometimes, that the following misunderstanding creates sometimes problems, where the developer thinks "I changed the object, so why my component doesn't rerender?":
When you create a new object like:
const user = { name: "john" };
You created an object that, has a property that points to the value "john" like this:
user -> { } -- name --> "john"
user points on an object and when you make name, point to a different value by:
user.name = "bob"
than user still points to the same object and to react it's the same object
but when you do
user = { ...user, name: "bob" };
then you would assign a new object and now it's a different object.

Look at using useEffect with a dependency of the prop that is being passed in. Within the effect, set the new question type in local state.
https://reactjs.org/docs/hooks-effect.html
Like this...
interface Props {
source: QuestionSource
}
export function QuestionsModal(props: Props) {
const [questionModel, setQuestionModel] = useState<QuestionModel>(new QuestionModel(questionSource))
useEffect(() => {
setQuestionModel(new QuestionModel(questionSource))
}, props.source)
}

Related

Vue.set() and push() for Vuex store

I have a mutator that attempts to make the follow update:
state.forms[1].data.metrics.push(newArrayItem)
forms is an object with 1 as a key
metrics is an array
For some reason, Vuex successfully updates, but components don't react to this change.
I was reading about Vue.set() on https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats
But I'm not sure how to apply it, or even if it's the right answer at all here.
Thanks for the help.
this.$forceUpdate is working, which is a bit strange because the component loading the data uses computed properties.
Form state is setup initially like so:
const state = {
forms: {}
};
New forms are pushed like so:
state.forms[id] = { formName: payload.formName, data: {metrics: []}};
And new metrics are added like so:
var result = getParent(state, payload.path);
result.metricParent.data.metrics.push({ id: newMetricId(state, result.formId), ...payload.metric });
Your problem is not with pushing to arrays. Your problem is in this line:
state.forms[id] = { formName: payload.formName, data: {metrics: []}};
Because you are creating a new property of state.forms without using Vue.set(), the form is not reactive, so when you push to the metrics array later, it's not recognized.
Instead, you should do this:
Vue.set(state.forms, id, { formName: payload.formName, data: {metrics: []}});
Then, pushing to state.forms[id].data.metrics should work as expected.
Vue setup reactive data looking for how the state/data is setup, by example if in a regular component you define the data like {x: {y: 10}} and you change the data somehow this.x.y = 20; it’s going to work, because Vue make the object with that structure reactive (because is the setup structure) based on that if you try to do, this.x.z = 10; not works because “z” not exists, and you need to tell to Vue that you need to make it reactive, this is when this.$set(this.x, “z”, 10); enters, it’s basically saying “make this data reference in position ‘z’ reactive”, after this point direct calls to this.x.z = ? works, in vuex the same happens, use Vue.set(state.forms, 1, { formName: payload.formName, data: {metrics: []}}); after that the reference to state.forms[1] (including sub data) is now reactive!

Vue - same mutation refreshes (or not!) components depending on which component it is called from?

I have problem understanding why THE SAME mutation fails to refresh data displayed in components (although it does change underlying vuex store data!) if it is called from one of the components, but it does refresh the data if called from another component?
I am updating Filter objects stored in store this way: state.report.filters[], where filters is array of Filter objects.
const state = {
report: {
filters: [], // array of Filter objects
...
}
}
My mutation looks for a filter in the array and substitutes the whole Filter object.
const mutations = {
setFilter: (state, newFilterValue) => {
let changedFilter = state.report.filters.find(filter => {
return filter.fieldName === newFilterValue.fieldName;
});
changedFilter = newFilterValue;
}
}
The mutation is called from a method of Filter class defined like this (separate module):
import { store } from './store';
export class Filter {
constructor ({
...
} = {}) {
this.operators = []; // Array of Operator objects
this.value = []; // Array of values - in this case Dates
};
updateOperator (operatorName) { // this mutation refreshes components when executed
this.operator[0] = new Operator(operatorName);
store.commit('setFilter', this); // whole object passed to the mutation
};
updateValue (newValue) { // this mutation changes store value, but fails to refresh components
this.value[0] = newValue; // newValue is a Date
store.commit('setFilter', this);
};
};
The app displays data in rows (each Filter has a separate row), each row contains cells, of which one contains components dedicated to Filter's value and Operator. These dedicated components receive as props callback functions which are methods of the Filter object. They execute the callback functions when a new value is entered passing the value to the Filter which then updates a relevant property and calls the mutation passing in both cases the whole Filter object as payload.
// TABLE CELL COMPONENT displaying filter value and operator
<template>
<td>
<operator-component
:iconName="proppedFilterObject.operator.iconName"
:callback="proppedFilterObject.updateOperator.bind(proppedFilterObject)"
></operator-component>
<value-component
:date="proppedFilterObject.value[0]"
:callback="proppedFilterObject.updateValue.bind(proppedFilterObject)"
></value-component>
</td>
</template>
<script>
export default {
props: ['proppedFilterObject'] // whole filter object
};
</script>
// OPERATOR COMPONENT
<template>
<div #click.stop="chooseOperator">
{{ iconName }} // some operator value display
</div>
</template>
<script>
export default {
methods: {
chooseOperator () {
const modal = new ChooseOperatorModal({
callback: this.callback // this displays another modal for receiving data. The modal calls the callback.
});
},
},
props: ['callback', 'iconName']
};
</script>
// VALUE COMPONENT
<template>
<date-picker v-model="computedDate"> // THIRD PARTY COMPONENT
</date-picker>
{{ date }} // additional display to verify if there's a problem within 'date-picker'
</template>
<script>
import DatePicker from 'vue2-datepicker'; // THIRD PARTY COMPONENT
export default {
components: { DatePicker },
computed: {
computedDate: {
get: function () {
return this.date;
},
set: function (newValue) {
this.callback(newValue);
}
}
},
props: ['callback', 'date']
};
</script>
So, if eg. I enter new operator value from Operator component, everything refreshes. When I enter a new value in the value component, the mutation is executed and store value changed, but displayed data are not refreshed. However, if afterwards I change an operator all the components will refresh and value will get displayed. Even if I change operator in a different Filter object(!). Ie:
a) Change in report.filters[0].value - display not refreshed, but...
b) then change report.filters[1].operator - both report.filters[1].operator AND PREVIOUSLY CHANGED report.filters[0].value get refreshed(?!).
What can be a reason of such behaviour? Where to look for the problem?
Some additional remarks:
1) I am using a third party component "vue2-date-picker" for date choice and display. However it does not seem to be responsible for the problem, as if I try to display the new value just in {{ }} notation it behaves the same way. I have used the date picker in other components and there it functions correctly as well.
2) In the code samples I left out most imports/exports and other seemingly irrelevant elements to keep the question reasonably short.
There are a lot of problems with the code and several of them are contributing to the problems you're seeing. A full, thorough answer that addresses all of these problems would be ridiculously long so instead I will skim through them without going into huge amounts of detail. You will need to do some further reading and experimentation to understand each of these topics properly.
Let's start with this line in the mutation:
changedFilter = newFilterValue;
This line assigns a new value to the local variable changedFilter. That's all. As it's the last line of the mutation the net result is that it doesn't really do anything.
Presumably your intent was to update the array state.report.filters, replacing the old entry with a new entry. However, just updating a local variable isn't going to do that.
At this point you may be wondering 'If that doesn't do anything, then why is the state in my store changing?'. I'll come to that in a moment but first let me prove to you that your existing code does nothing.
Try removing the code inside setFilter completely. Just leave an empty function. Then try clicking around in the UI just like you did before. You'll find that the store state updates just the same as it did before, even though you've removed the code to update the array.
The correct way to implement that mutation would be to use findIndex to find the relevant index and then use either Vue.set or the array's splice method to update the array accordingly. That will change the item in the array. However...
This brings us back to the earlier question. Why is the state updating if the mutation does nothing?
This is because you're using the same object in multiple places. The Filter object held in the array is the same object that your UI is editing. There are no copies being taken, there is just a single object. So when you change the properties of that object inside updateOperator or updateValue this will immediately be reflected inside the store. Calling the setFilter mutation is just asking the store to replace an object with itself.
There's nothing specific to Vue about this. This is just the standard behaviour of reference types in JavaScript. It is also common with many other programming languages that don't directly expose pointers. It can be useful to learn a little about how pointers work in other languages as it will give you a better initial mental model before attempting to understand how reference types behave in JavaScript. Understanding the difference between 'by value' and 'by reference' may also be a useful starting point.
The next topic to cover is reactivity, which very much is a Vue topic.
Specifically, there are certain changes that Vue can't detect. These are usually referred to as the reactivity caveats. You can find more about them in the official documentation:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
https://v2.vuejs.org/v2/guide/list.html#Caveats
There are at least two lines in your code that violate these rules:
this.operator[0] = new Operator(operatorName);
and
this.value[0] = newValue;
You can't set array entries directly by index. The array will update but it won't trigger any reactive dependencies within Vue. Instead you need to use either Vue.set or one of the array methods, e.g. push, pop, splice, etc.. In this example you could use splice.
e.g. Using Vue.set:
Vue.set(this.value, 0, newValue);
e.g. Using splice:
this.value.splice(0, 0, newValue);
Why does all of this matters?
Well Vue will only re-render a component if its reactive dependencies have changed. They are very similar to computed properties in that regard. Here's how it works...
Vue compiles the template down to a function. That function is referred to as the render function. When rendering a component Vue calls the render function and that function returns a description of how to render the component. Any reactive properties that are touched while that function is running will be recorded as dependencies. If, at some point in the future, the value of one of those reactive properties changes then Vue will rerun the render function to generate a new rendering of that component.
There are two key points to take out of this description:
If you fall foul of one of the reactivity caveats then Vue won't know the dependency has changed, so it won't re-render the component.
The render function runs as a whole. It doesn't just target a small chunk of the template, it always runs the whole thing.
So if you change a dependency in a non-reactive way (i.e. one of the caveats) it won't trigger a rendering update. But if you subsequently update a dependency properly, Vue will detect that and will rerun the render function. When it runs it will run the whole thing, so any new values will be picked up, even if they weren't detected when they changed.
It isn't immediately clear to me which rendering dependency is causing your component to re-render. However, it only needs one of them to change in a detectable manner. Any other changes will then get pulled in incidentally when the render function runs and reads their current values.
That covers why your code isn't working. However, I would also worry about your decision to introduce a Filter class. I understand how that may be appealing if you've come from some other OO environment but it isn't typically how Vue is used. It is possible to make it work but you will need a good understanding of both JavaScript reference types and the Vue reactivity system to avoid falling through the cracks. There is no reason why using a specific class to hold your data can't be made to work but in practice it usually ends up being less maintainable than not using such a class. A more typical Vue approach would be to use simple, anonymous objects/arrays to hold the data and then for the data owner (either a component or store module) to be responsible for making any mutations to that data. Events are used to pass changes up the component hierarchy rather than callback props.
Ultimately you will need to judge whether the Filter class is justified but it is probably not what future maintainers of your code will be expecting.

Vue.js extend component and data updates

I'm using vue.js extends for the first time. I have a component that extends another and it needs to read the root components data to update the status in its own component.
What I'm finding is that the component that extends the other only seems to take a copy of the root's data when it's rendered but if I update a property in the root component it's not updated in the extended component.
So I might not be going about this the right way if the extended component doesn't update when the root does. For example I want to check the length of an array on the root component and update another data value. It updates the value on the root but not on the extended component.
Is this the expected behaviour and is there a way I can send the updated data down to the extended component?
Sample code:
<a inline-component>
<input type="text" v-model="myArray" />
<button v-on:click="saveData">Save</button>
</a>
<b inline-component>
<div v-if="myArray.length > 0">On Target</div>
</b>
var a = Vue.component('a', {
data: function () {
return {
myArray: [],
}
},
methods: {
saveData : function(){
var vm = this;
axios.post('/save', {
})
.then(function (response) {
vm.myArray = response.data;
})
.catch(function (error) {
console.log(error);
});
},
}
});
Vue.component('b', {
extends: a,
});
I have a component that extends another and it needs to read the root components data to update the status in its own component.
For the purposes of my answer I'm going to assume that you have two component definitions, A and B, and B extends A. I assume that when you say root you just mean A.
What I'm finding is that the component that extends the other only seems to take a copy of the root's data when it's rendered but if I update a property in the root component it's not updated in the extended component.
Rendering is not really relevant here. The data properties are set up when a component instance is created. Typically rendering will happen just after creation but merging any data happens much earlier in the component life-cycle. Even if the component isn't rendered the data will still be initialised.
No copying takes place. Let's consider a data function on component A:
data () {
return {
myArray: []
}
}
Every time this function is invoked it is going to return a new object, each containing a new array. This is precisely what happens if you create an instance of A directly. For each instance, Vue will call this function and get a new object defining the data. Generally that's what you'd want, rather that having components sharing data.
Now let's consider B. That might define its own data function. When an instance of B is created Vue will call the data function for both A and B and then merge the objects. No copying takes place, just merging. If you want to know more about how Vue handles merging in general see the documentation but for data the strategy is pretty simple. Properties from both objects will be combined with B taking precedence over A if there's a clash of property names. There is no recursive merging of properties.
So the idea of updating 'a property in the root component' is not particularly well-defined. You might be thinking of it as a bit like a prototype chain, where modifying a superclass would impact the subclass, but that isn't what's going on here. The data functions are invoked when the component is created and that's that. There isn't a lasting link back to the component definition like there is with a prototype chain.
If you really want all your component instances to share the same data value then it can be done, you just need to make sure that the data function is returning the same object/array every time. e.g.
const myArray = []
export default {
name: 'A',
data () {
return {
myArray
}
}
}
Written this way all instances of A will share the same array for myArray. So long as B doesn't define it's own value for myArray it will share it too.
For example I want to check the length of an array on the root component and update another data value. It updates the value on the root but not on the extended component.
I'm struggling a bit to understand what that means. It seems there are lots of assumptions about things being shared, single instances here. It's not entirely clear how you update the 'root' given it's a component definition and not a component instance.
If possible you should use a computed property for this. That would be inherited by B. Each instance of A (or B) would have their own value for this computed property, which might be a little wasteful if they're all going to be the same, but it's probably still the best way to go.
You could in theory use a watch. That should be inherited too but keep in mind it would be manipulating values for that particular instance.
Reading between the lines a little, if you wanted to update something on the 'root' so that it magically appeared in the subcomponents you could use the same shared reference-type trickery that I demonstrated earlier for myArray. You may need to be careful with how you update it though. If, for example, you used a watch you might find the you end up updating the same object many times, once for each instance of the component.
Update:
Based on the code you've posted it could be made to work something like this:
var myArray = [];
var a = Vue.component('a', {
data: function () {
return {
myArray: myArray // Note: using the same, shared array
}
},
methods: {
saveData : function(){
var vm = this;
axios.post('/save', {
})
.then(function (response) {
// Note: Updating the array, not replacing it
var myArray = vm.myArray;
myArray.splice(0, myArray.length);
myArray.push.apply(myArray, response.data);
})
.catch(function (error) {
console.log(error);
});
},
}
});
Vue.component('b', {
extends: a,
});
Your example didn't include any ES6 so I've refrained from using it but it would be a bit simpler if that were available.
The example above works by sharing the same array between all instances of the component and then mutating that instance. Assigning a new array to that property won't work as it would only update that particular component instance.
However, all that said, this is increasingly looking like a case where you should give up on trickery and just use the Vuex store instead.

How does the next() guard function in vue-router work?

I'm trying to understand the solution in this SO post. The solution allows the user to keep track of the previous route in the current route.
Below is the snippet of Vue code that I'm trying to understand. If I understand correctly, next accepts a callback function that receives the current component's vue instance. We then set the prevRoute data property of this vue instance to from. Is this interpretation correct? If not, what is actually happening?
If someone could also add a brief explanation as to what the Vue API is doing behind the scenes that would also be very helpful for me to actually understand the snippet of code.
...
data() {
return {
...
prevRoute: null
}
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.prevRoute = from
})
},
...
As per the documentation...
The beforeRouteEnter guard does NOT have access to this, because the guard is called before the navigation is confirmed, thus the new entering component has not even been created yet.
However, you can access the instance by passing a callback to next. The callback will be called when the navigation is confirmed, and the component instance will be passed to the callback as the argument
So vm is the component instance assigned to the destination route.
From your question...
We then set the prevRoute data property of this vue instance to from. Is this interpretation correct?
Almost. All you're doing is setting a direct object property on the Vue component which is after all, just a JavaScript object at heart. For example
const vm = { name: 'I am totally a Vue component' }
vm.prevRoute = from
This property will not be reactive but you can certainly access it within your component via this, just as you can other non-data properties like $el, $refs, etc.

Creating local copy of passed props in child component in vue.js?

In vue.js what is the right way to edit prop without changing parent data?
What I mean by that is whenever we pass any property from parent to child in vue.js then if we make any change to that property in child component then the change is also reflected in parent's component.
Is there any way in vue.js to make a local copy of passed property in a child?
I googled this but everywhere it is written that we can achieve this by doing this.
props:["user"],
data(){
return {
localUser: Object.assign({}, this.user)
}
}
here the user is passed an object and I am creating a copy of it in local user but it doesn't work at all, the local user is undefined.
Have you encountered a scenario like this where you have to make changes to a parent property in child component without affecting the state of parent component i.e- making your own copy in child and then edit it?
Any insights on this will be helpful.
I have also read somewhere that in In vue#2.3.3,when we want to pass a prop from Father to Child component, we need to manually create a local data to save the prop, that makes lots of useless works.
we can maually create the local data like this :
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}
but this is not working in my case as well.
I am using vue cli 3.0.1 for the developemnt purpose.
Here is my code for the same.
In my application I have a list view.
When user clicks on the See Focused View button user is redirected to below mentioned view i.e is actaully a bootstrap - modal view.
Here user can edit the value of Name, but as I am passing name here as a property from aprent component so editing it here causes it to update on parent component as well i.e in the list view as well.
In your fiddle, the child component is using Object.assign() to create a copy of data, which is an array of objects. However, this only creates a shallow copy, so the array elements would still refer to the original instances, leading to the behavior you're seeing.
A few solutions to deep copy the array:
Use JSON.parse(JSON.stringify(this.data)), which works reasonably well for primitive object properties (String, Number, BigInt, Boolean, undefined, and null):
data() {
return {
local_data: JSON.parse(JSON.stringify(this.data))
}
}
(demo 1)
Map the objects into new ones, which works well if the depth is only 1 (nested arrays/objects will still be shallow copied):
data() {
return {
local_data: this.data.map(x => ({...x}))
}
}
(demo 2)
Use a utility library, such as lodash's cloneDeep:
data() {
return {
local_data: _.cloneDeep(this.data)
}
}
(demo 3)