Vue.js - Add or set new data to array in store - javascript

I have two Vue components that use a common array set in a store like this:
var store = {
state: {
myArray: []
}
};
var FirstComp = Vue.extend({
template: '#first-template',
data: function () {
return {
arrayData: store.state.myArray
};
}
});
/* A second component quite identical */
I followed the example given in the Vue js guide.
I'm trying to update the data in the array in the store with new data from another array (after an ajax call), so that it impacts both components. I would like to have a nice way of replacing / concating the old array with a new one. I know I can't just replace the array like this store.state.myArray = newArrayData;because I would loose the Vue binding. But the method given in the docs (at least for concat) doesn't work in the case of the store (or maybe I'm missing something?).
Right now, the only way I've found is to use a foreach with push, $removeor $set depending on the operation and it is not that elegant and practical.
For example, for concat, I do this:
$.each(newArray, function (i, val) {
store.state.myArray.push(val);
});
But for replacing it gets uglier. What would be the proper way to this?
(For info, I'm not using Vuex for state management and I don't plan to at the moment, I'm keeping it very simple)

To make the assignment work you can just use "state" in your component like this:
var FirstComp = Vue.extend({
template: '#first-template',
data: function () {
return {
state: store.state
};
}
});
And then use state.myArray. This way if you will do store.state.myArray = newValue it won't break the binding.

Related

Vuex: store.state.activities has a key "list" that is an array of 3 items, however store.state.activities.list returns an empty array

My project is in Vue.
store.state.activities is an object that has 2 keys, one of them is an array called list that has 3 items.
However, when I try to reference it using store.state.activities.list, I get an empty array.
I've tried making both a shallow and a deep copy of store.state.activities, in both cases the copy has an empty list array.
store.state.activities structure:
{
"list": [
{
// some data
},
{
// some data
},
{
// some data
}
],
"dictionaries": {}
}
console.log(store.state.activities) - you can see list has 3 items:
whereas store.state.activities.list returns an empty array:
the usual reasons for this are you've not initialised or accessed the vuex correctly
initialising
is usually done in your main file and should look something like
import { store } from "../store";
createApp(App)
.use(store)
.mount("#app");
accessing
if you are using the Options API you have to access the store via the this in the components, which looks as so
this.$store.state.activities
if you are using the composition API however the this object is not available in which case you should do something like
import {useStore} from "vuex"
...
setup(){
const store = useStore();
return {
activities : computed(()=>store.state.activities),
...
if you are just importing the store object you have defined then you will be getting a uninitialised version that wont be picking up any changes you make
if using type script the code is a little different
the final posibility i can think of is your assignment to the list property
if you are doing
this.$store.state.activities.list = [{},{}.{}];
then you will be replacing the list which will break the reference, assignment should be done via a mutator as vuex will wrap these changes in reactive wrapers that tell watchers of changes
mutations: {
setList(state, value) {
state.activities.list = value;
},
...
},
which would then be called as
store.commit("setList", [{},{},{}]);

Vue Reactivity: Why replacing an object's property (array) does not trigger update

I have a Vue app where I'm trying to make a thin wrapper over the Mapbox API. I have a component which has some simple geojson data, and when that data is updated I want to call a render function on the map to update the map with that new data. A Vue watcher should be able to accomplish this. However, my watcher isn't called when the data changes and I suspect that this is one of the cases that vue reactivity can't catch. I'm aware that I can easily fix this problem using this.$set, but I'm curious as to why this isn't a reactive update, even though according to my understanding of the rules it should be. Here's the relevant data model:
data() {
return{
activeDestinationData: {
type: "FeatureCollection",
features: []
}
}
}
Then I have a watcher:
watch: {
activeDestinationData(newValue) {
console.log("Active destination updated");
if (this.map && this.map.getSource("activeDestinations")) {
this.map.getSource("activeDestinations").setData(newValue);
}
},
}
Finally, down in my app logic, I update the features on the activeDestination by completely reassigning the array to a new array with one item:
// Feature is a previously declared single feature
this.activeDestinationData.features = [feature];
For some reason the watcher is never called. I read about some of the reactivity "gotchas" here but neither of the two cases apply here:
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
What am I missing here that's causing the reactivity to not occur? And is my only option for intended behavior this.set() or is there a more elegant solution?
as default vue will do a shallow compare, and since you are mutating the array rather than replacing, its reference value is the same. you need to pass a new array reference when updating its content, or pass the option deep: true to look into nested values changes as:
watch: {
activeDestinationData: {
handler(newValue) {
console.log("Active destination updated");
if (this.map && this.map.getSource("activeDestinations")) {
this.map.getSource("activeDestinations").setData(newValue);
}
},
deep: true
}
}
If you need to watch a deep structure, you must write some params
watch: {
activeDestinationData: {
deep: true,
handler() { /* code... */ }
}
You can read more there -> https://v2.vuejs.org/v2/api/#watch
I hope I helped you :)

Vue component's prop set to instance data mutates the upstream value

I am seeing some weird behaviour here that was unexpected, but it makes intuitive-sense to me in terms of pure JavaScript.
I have a form controller that accumulates a this.thing object that is sent to the server on the final submit. It's a multi-step form, so each step adds some data to this.thing.
So the controller has:
data() {
return {
thing: {},
};
},
The DOM markup for this controller has a child like:
<a-child
:initial-thing="thing"
></a-child>
The child uses that prop to display its initial state, so it receives the prop and sets it into its own local state as instance data:
initialThing: {
type: Object,
required: true,
},
...
data() {
return {
thing: this.initialThing,
};
},
Then this child has a checkbox that is like this:
<a-checkbox
v-model="thing.field"
:initial-value="initialThing.field"
></a-checkbox>
This all works fine, except I just noticed that when the checkbox changes, it's mutating the parent controllers thing.field value.
I'm making this question because I don't understand how Vue can do that, and the only thing that makes sense to me is that when the child does thing: this.initialThing, it's allowing the child to call the setter function on that field on this.initialThing.
It stops mutating the parent's state if I do this instead:
data() {
return {
thing: { ...this.initialThing },
};
},
In my actual app, it's more complex because there are 2 intermediate components, so the grandchild is mutating the grandparent's state, and it stems from the pattern I am describing here.
Can anyone provide a kind of textbook answer for what is happening here? I'm hesitant to rely on this behaviour because the code driving it is not explicit. It makes some of my $emit() events redundant in favour of using this indirect/non-explicit way of sending data upstream.
Also to be clear, this has nothing to do with v-model because it also does it if I do this.thing.field = 'new value';. I believe it has everything to do with inheriting the getters/setters on this.initialThing. Is it safe to rely on this behaviour? If I rely on it, it will make my code more concise, but a naive individual may have a hard time understanding how data is making it into the grandparent component.
This is a shallow copy so you can't prevent mutating grandchildren.
data() {
return {
thing: { ...this.initialThing },
};
},
The solution is below:
data() {
return {
thing: JSON.parse(JSON.stringify(this.initialThing)),
};
},
const initialThing = {
age: 23,
name: {
first: "David",
last: "Collins",
}
}
const shallowCopy = { ...initialThing };
shallowCopy.age = 10;
shallowCopy.name.first = "Antonio"; // will mutate initialThing
console.log("init:", initialThing);
console.log("shallow:", shallowCopy);
const deepCopy = JSON.parse(JSON.stringify(initialThing));
deepCopy.age = 30;
shallowCopy.first = "Nicholas"; // will not mutate initialThing
console.log("------Deep Copy------");
console.log("init:", initialThing);
console.log("deep:", deepCopy);
How it works:
JSON.stringify(this.initialThing)
This converts JSON Object into String type. That means it will never mutate children anymore.
Then JSON.parse will convert String into Object type.
But, using stringify and parse will be expensive in performance. :D
UPDATED:
If you are using lodash or it is okay to add external library, you can use _.cloneDeep.
_.cloneDeep(value); // deep clone
_.clone(value); // shallow clone

How to clear array which is inside nested state?

I have crated state like below and at some point I want to clear this array and update with new one. I used below code for clearing, but when I check state through react-developer tool, array is still there nothing is happening. Please help me as my newbie to ReactJS.
state = {
formData:{
level:{
element:'select',
value:'Bachelors',
label:true,
labelText:'Level',
config: {
name:'department',
options: [
{val :"Bachelors", text:'Bachelors'},
{val:"Masters", text:'Masters'}
]
},
validation: {
required:false,
}
,
valid:true,
touched:false,
validationText:'',
},
Now I want to clear options array by:
let {options} = this.state.formData.level.config
options = []
this.setState({
options:options
})
But my array is not clearing. How can I achive that?
Two things that you should know.
1) When you do this.setState({ options: options }), you are not updating the options array belonging to the formData object. You are creating a brand new key-value pair in your state called options. If you try to print your updated state, you will find that your state now looks like:
state = { formData: {...}, options: [] }
2) You are trying to directly mutate state, (changing a value belonging to the existing object). This is against React principles, because in order for the component to re-render correctly without being prone to side-effects, you need to provide a brand new state object.
Since its so deeply nested, the likely best option is to deep clone your state-object like this:
const newFormData = JSON.parse(JSON.stringify(this.state.formData))
newFormData.level.config.options = []
this.setState({
formData: newFormData
})
This will probably be the safest way to clear your form data object. Here's a working sandbox with this feature as well. Please use it for reference:
https://codesandbox.io/s/naughty-cache-0ncks
create newstate and change properties then apply the new state to setState() function like this:
changestate = ()=>{
var newstate = this.state;
newstate.formData.level.config.options=["hello","wow"]
this.setState(newstate)
}
You are not clearing the options state but creating a new state with an empty array.
You can either clone the whole object using JSON stringify and JSON parse too . Other way to update nested states without mutation is by using spread operators. The syntax looks a bit complex though as spread nested objects while preserving everything is tricky.
const newFormData = {
...this.state.formData,
level: {
...this.state.formData.level,
config: {
...this.state.formData.level.config,
options: []
}
}
};
this.setState({
formData: newFormData
})

Vue data not available until JSON.stringify() is called

I'm not sure how to tackle this issue because there's quite a bit into it, and the behavior is one I've never seen before from JavaScript or from Vue.js
Of course, I will try to keep the code minimal to the most critical and pieces
I'm using vue-class-component(6.3.2), so my Vue(2.5.17) components look like classes :)
This particular component looks like so:
import GameInterface from '#/GameInterface';
class GameComponent extends Vue {
public gameInterface = GameInterface();
public mounted() {
this.gameInterface.launch();
}
}
GameInterface return an object with a launch method and other game variables.
In the game interface file to method looks something like this:
const GameInterface = function () {
const obj = {
gameState: {
players: {},
},
gameInitialized: false,
launch() => {
game = createMyGame(obj); // set gameInitialized to true
},
};
return obj;
}
export default GameInterface;
Great, it works, the object is passed onto my Phaser game :) and it is also returned by the method, meaning that Vue can now use this object.
At some point I have a getter method in my Vue class that looks like so:
get currentPlayer() {
if (!this.gameInterface.gameInitialized) return null;
if (!this.gameInterface.gameState.players[this.user.id]) {
return null;
}
return this.gameInterface.gameState.players[this.user.id];
}
And sure enough, null is returned even though the player and id is clearly there.
When I console.log this.user.id I get 4, and gameInterface.gameState.players returns an object with getters for players like so:
{
4: { ... },
5: { ... },
}
Alright, so it does not return the player even though the object and key are being passed correctly...
But I found an extremely strange way to "FIX" this issue: By adding JSON.parse(JSON.stringify(gameState)) like so
get currentPlayer() {
// ...
if (!this.gameInterface.gameState.players[this.user.id]) {
// add this line
JSON.stringify(this.gameInterface.gameState);
return null;
}
return this.gameInterface.gameState.players[this.user.id];
}
It successfully returns the current player for us... Strange no?
My guess is that when we do this, we "bump" the object, Vue notices some change because of this and updates the object correctly. Does anyone know what I'm missing here?
After working on the problem with a friend, I found the underlying issue being a JavaScript-specific one involving Vue's reactive nature.
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
In this section of the documentation, a caveat of Vue's change detection is discussed:
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.
When, in my game run-time, I set players like so:
gameObj.gameState.players[user.id] = {...playerData}
I am adding a new property that Vue has not converted on initialization, and Vue does not detect this change. This is a simple concept I failed to take into account when developing my game run-time.
In order to correctly set a new player, I've decided to use the spread operator to change the entirety of the players object, which Vue is reacting to, and in turn, Vue will detect my player being added like so:
gameObj.gameState.players = {
...gameObj.gameState.players,
[user.id]: {...playerData}
}
Vue also discusses another method called $set, which you can read on the same page.

Categories

Resources