I'm rendering a list based on an object. I'm adding new elements to this object by help of the mutations. When I log this.$store.state.myObject to console, I can see that it is updating. However, my list is not updated.
Actually, I got lucky and I fixed this issue by adding the line of code below in my mutation. (I found out that this can help DOM update.)
But I'd like to learn if this is a good solution.
state.realms = Object.assign({}, state.realms)
Here is my whole mutation:
addToRealms: (state, val) => {
var id = val.id
var realmName = val.name.en_US
state.realms[id] = { name: realmName }
state.realms = Object.assign({}, state.realms)
}
And here is my vue page code
<q-btn flat
v-for="(realm,code) in realms"
:key="code"
#click="selectRealm(realm)"
:label="realm.name"
clickable/>
I defined realms as a computed property.
computed: {
realms () {
return this.$store.state.realms
}
Further info:
I use vue.js devtools extension, when I track my commit on vuex store I can see that I'm really changing the state. But It doesnt affect immediately. If I press commit all button, my list gets updated.
Thanks.
When you use
state.realms = Object.assign({}, state.realms)
you basically are recreating a new object based on the previous one (breaking the references in case there are not nested objects), and that's the reason why in this way the list get updated.
Give a read to this article to understand more about deep-cloning and breaking references.
The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object
When you add new properties to the object, those are not reactive. To make them reactive, use the Vue.set() method, (docs here).
Vue.set(state.realms[id],'name', realmName);
Related
assuming I have a React app that has a internal state where I store an object like
const [car, setCar] = useState(new Car());
Assuming my class Car looks like the following:
class Car {
constructor(brand) {
this.carname = brand;
}
present() {
return "I have a " + this.carname;
}
}
When I run and debug the app, I can save a Car object into the state and also retrieve it and call present().
Now, when I make changes to the present() function, e.g.
present() {
return "I have a " + this.carname + " and it is shiny";
}
then due to Fast Refresh my App gets refreshed. But unfortunately, because the object is already stored in state, it will not receive the update of the function implementation.
Is there a way that I can change the code so that with a Fast Refresh the function implementation will be updated also for the object in React state?
I tried updating the method via prototype but it did also not work.
This is actually a bit tricky since what you are trying to achieve goes somewhat against react's design around immutable. Directly changing a classes method and assigning it to an existing instance can easily be done using Object.setPrototypeOf(car, Car.prototype).
This replaces the original car instance's prototype and changes the method implementation. As you've said this does not work because the mutating an object does not trigger a rerender.
useState compares objects using Object.is() which won't trigger a rerender in this case since both objects have the same reference.
For the sake of Object.is(), you need to pass in a new object with the new prototype and exact properties of the existing instance to setCar(). This can't be done using the basic spread operator {..car} or Object.assign({}, car) since these methods only copy an object's own enumerable properties.
You can instead use the following to create a new object with the exact same properties the new prototype setCar(Object.assign(Object.create(Car.prototype), car))
This can be placed inside useEffect which will run everytime the original Car class is changed
const [car, setCar] = useState(new Car());
useEffect(() => {
setCar(Object.assign(Object.create(Car.prototype), car))
}, [])
By design, Fast Refresh will preserve state whenever it can. useEffect however will always update during a Fast Refresh regardless of what is in the dependancy array. As long as you provide an empty dependency array, you can use setState inside of useEffect to change state between fast refreshes.
const [car, setCar] = useState(new Car());
useEffect(() => {
setCar(new Car());
},[])
Keep in mind though, useEffect will only run after the first render so you need to keep the default state new Car() in the useState hook argument to prevent any undefined errors.
I'm currently building a Vue app that consumes data from the Contentful API. For each entry, I have a thumbnail (image) field from which I'd like to extract the prominent colours as hex values and store them in the state to be used elsewhere in the app.
Using a Vuex action (getAllProjects) to query the API, run Vibrant (node-vibrant) and commit the response to the state.
async getAllProjects({ commit }) {
let {
fields: { order: order }
} = await api.getEntry("entry");
let projects = order;
projects.forEach(p =>
Vibrant.from(`https:${p.fields.thumbnail.fields.file.url}`)
.getPalette()
.then(palette => (p.fields.accent = palette.Vibrant.hex))
);
console.log(projects);
// Commit to state
commit("setAllProjects", projects);
}
When I log the contents of projects right before I call commmit, I can see the hex values I'm after are added under the accent key. However, when I inspect the mutation payload in devtools, the accent key is missing, and so doesn't end up in the state.
How do I structure these tasks so that commit only fires after the API call and Vibrant have run in sequence?
You cannot add a property to an object in Vue and have it be reactive; you must use the Vue.set method.
Please try replacing that forEach block with the following, which adds the new property using Vue.set:
for (i=0; i<projects.length; i++)
Vibrant.from(`https:${projects[i].fields.thumbnail.fields.file.url}`)
.getPalette()
.then(palette => (Vue.set(projects[i].fields, accent, palette.Vibrant.hex)))
);
UPDATE: changing the format from forEach to a conventional for loop may be gratuitous in this case, since the assignment being made is to an object property of projects and not to a primitive.
I'm not spending a lot of time on StackOverflow, and if the above answer works, I am happy for you indeed.
But I expect from that answer you will get console warnings telling you not to mutate state directly.
Now when this happens, it's because while Vue.set(), does in fact help Vue understand reactively a change has been made, potentially deeply nested in an object.
The problem here is that since you are looping the object, changing it all the time, the commit (Mutator call) is not the one changing state - Vue.set() is actually changing it for every iteration.
I have a observable store that is being based via props to components that need it.
Console logging from components does show store as expected, but only if I stick to the whole store. Once I start chaining into it I get undefined.
Base store
export let TutorStore = observable({
Tutor: {},
Queue: [],
QLength: null
});
Component logging
checkBtn = () => {
console.log(this.props.tutorStore);
};
Result TutorStore in Console as expected
All the correct tutorStore objects are there, with the right data, as expected.
But if I try to chain into a object there are no values attached to it, not behaving as expected.
checkBtn = () => {
console.log(this.props.tutorStore.Tutor);
};
Result TutorStore.Tutor no values
I've tried messing around with mobx's toJS method, but it seems unreliable at best.
Considering just assigning the appropriate object to components state, but that defeats the purpose of having a store.
The data is there, so how do I access it?
Let's assume we have some array of objects, and these objects never change. For example, that may be search results, received from google maps places api - every result is rather complex object with id, title, address, coordinates, photos and a bunch of other properties and methods.
We want to use vue/vuex to show search results on the map. If some new results are pushed to the store, we want to draw their markers on the map. If some result is deleted, we want to remove its marker. But internally every result never changes.
Is there any way to tell vue to track the array (push, splice, etc), but not to go deeper and do not track any of its element's properties?
For now I can imagine only some ugly data split - keep the array of ids in vue and have separate cache-by-id outside of the store. I'm looking for a more elegant solution (like knockout.js observableArray).
You can use Object.freeze() on those objects. This comes with a (really tiny!) performance hit, but it should be negligible if you don't add hundreds or thousands of objects at once.
edit: Alternatively, you could freeze the array (much better performance) which will make Vue skip "reactifying" its contents.
And when you need to add objects to that array, build a new one to replace the old one with:
state.searchResults = Object.freeze(state.searchResults.concat([item]))
That would be quite cheap even for bigger arrays.
At the second glance data split seems not so ugly solution for this task. All that we need is using getters instead of the raw vuex state. We suppose that incoming results is an array with any objects that have unique id field. Then the solution could look like:
const state = {
ids: []
}
let resultsCache = {};
const getters = {
results: function(state) {
return _.map(state.ids,id => resultsCache[id]);
}
}
const mutations = {
replaceResults: function(state,results) {
const ids = [];
const cache = {};
(results||[]).forEach((r) => {
if (!cache[r.id]) {
cache[r.id] = r;
ids.push(r.id);
}
});
state.ids = ids;
resultsCache = cache;
},
appendResults: function(state,results) {
(results||[]).forEach((r) => {
if (!resultsCache[r.id]) {
resultsCache[r.id] = r;
state.results.push(r.id);
}
});
}
}
export default {
getters,
mutations,
namespaced: true
}
I created a fork out of vue called vue-for-babylonians to restrict reactivity and even permit some object properties to be reactive. Check it out here.
With it, you can tell Vue to not make any objects which are stored in vue or vuex from being reactive. You can also tell Vue to make certain subset of object properties reactive. You’ll find performance improves substantially and you enjoy the convenience of storing and passing large objects as you would normally in vue/vuex.
You can use shallowRef to achieve this.
First import it:
import {shallowRef} from 'vue';
In your mutations you can have a mutation like this:
mutations: {
setMyObject(state, payload) {
state.myObject = shallowRef(payload.value);
},
}
This will track replacing the object, but not changes to the objects properties.
For completeness here is the documentation to shallowRef:
https://v3.vuejs.org/api/refs-api.html#shallowref
My code works, but I have a best practice question: I have an array of objects in the state, and a user interaction will change a value of one object at a time. As far as I know, I'm not supposed to change the state directly, i should always use setState instead. If I want to avoid that with any price, I will deep clone the array by iteration, and change the clone. Then set the state to the clone. In my opinion avoiding to change the state that I will change later anyway is just decreasing my performance.
Detailed version:
this.state.data is an array of objects. It represents a list of topics in a forum, and a Favorite button will toggle, calling clickCollect().
Since I have an array in the state, when I change the is_collected property of one item, I need to create a copy of the array to work with, and after changing to the new value, I can set it to the state.
var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});
var data = this.state.data : This would copy the pointer to the array and push(), shift(), etc would alter the state directly. Both data and this.state.data will be affected.
var data = this.state.data.slice(0) : This makes a shallow clone, push and shift doesn't change the state but in my clone I still have pointers to the elements of the state's array. So if I change data[0].is_collected, this.state.data[0].is_collected gets changed as well. This happens before I call setState().
Normally I should do:
var data = [];
for (var i in this.state.data) {
data.push(this.state.data[i]);
}
Then I change the value at index, setting it to true when it's false or false when it's true:
data[index].is_collected = !data[index].is_collected;
And change state:
this.setState({data: data});
Consider my array is relatively big or enormously big, I guess this iteration will reduce the performance of my APP. I would pay that cost if I knew that it is the right way for any reason. However, in this function (clickCollect) I always set the new value to the state, I'm not waiting for a false API response that would say to stop making the change. In all cases, the new value will get into the state. Practically I call setState only for the UI to render again. So the questions are:
Do I have to create the deep clone in this case? (for var i in ...)
If not, does it make sense to make a shallow clone (.slice(0)) if my array contains objects? The changes are being made on the objects inside of the array, so the shallow clone still changes my state, just like a copy (data = this.state.data) would do.
My code is simplified and API calls are cut out for simplicity.
This is a beginner's question, so a totally different approach is also welcome. Or links to other Q & A.
import React from 'react';
var ForumList = React.createClass({
render: function() {
return <div className="section-inner">
{this.state.data.map(this.eachBox)}
</div>
},
eachBox: function(box, i) {
return <div key={i} className="box-door">
<div className={"favorite " + (box.is_collected ? "on" : "off")} onTouchStart={this.clickCollect.bind(null, i)}>
{box.id}
</div>
</div>
},
getInitialState: function() {
return {data: [
{
id: 47,
is_collected: false
},
{
id: 23,
is_collected: false
},
{
id: 5,
is_collected: true
}
]};
},
clickCollect: function(index) {
var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});
}
});
module.exports = ForumList;
Personally I don't always follow the rule, if you really understand what you are trying to do then I don't think it's a problem.
var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});
In this case, mutating state and calling the setState again like this is fine
this.state.data[index].is_collected = !this.state.data[index].is_collected;
this.setState({data: this.state.data});
The reason you should avoid mutating your state is that if you have a reference to this.state.data, and calling setState multiple times, you may lose your data:
const myData = this.state.data
myData[0] = 'foo'
this.setState({ data: myData })
// do something...
// ...
const someNewData = someFunc()
this.setState({ data: someNewData })
myData[1] = 'bar' // myData is still referencing to the old state
this.setState({ data: myData }) // you lose everything of `someNewData`
If you really concerned about this, just go for immutable.js
Muting the state directly breaks the primary principle of React's data flow (which is made to be unidirectional), making your app very fragile and basically ignoring the whole component lifecycle.
So, while nothing really stops you from mutating the component state without setState({}), you would have to avoid that at all costs if you want to really take advantage of React, otherwise you would be leapfrogging one of the library's core functionalities.
If you want follow react best practices, you should do shallow copy of all your array, when you change any property. Please look into "immutable" library implementation.
But, from my experience, and from my opinion, setState method should be called if you have "shouldCompomenentUpdate" implementations. If you think, that your shallow copy will be consume much more resources, then react virtual dom checks, you can do this:
this.state.data[0].property = !this.state.data[0].property;
this.forceUpdate();
If I understood your question right, you have an array of objects and when a property of a single object in array changes,
Create a deep clone of the array and pass to setState
Create a shallow clone and pass to setState
I just checked with the redux sample todo app and in case of a single property of an object changes you've to create a fresh copy of that single object not the entire array. I recommend you to read about redux and if possible use to manage the state of your app.