Vue not updating global state - javascript

I have some problems with updating the global state. I'm trying to update that state by listening WebSocket, but it's not updating as expected.
Here is how did I define the global state.
state: {
userData: null
},
getters: {
userData: state => {
return state.userData
},
mutations: {
GET_USER({payload}) {
commit('GET_USER', payload)
},
And I'm updating it in App.vue like so:
mounted() {
window.Echo.channel("user." + this.userData.id).listen(".user-updated", (user) => {
this.$store.commit('GET_USER', JSON.stringify(user.user))
});
Ofcourse I'm closing that websocket. I tried with localStorage, which I think is not a bad idea, but still I'm doing it with global state, and with localstorage would look like:
localStorage.setItem('userData', JSON.stringify(user.user))
So when I want to show that data in some component, for example, Home.vue, the Only way that I can see what is happening, is by defining {{ this.$store.getters.userData }} in the template of that file, but If I try to define it in scripts data, like so:
data() {
return {
data: this.$store.getters.userData,
}
},
It's not updating real time, but only if I go to another page and return here, or update the component.
Any ideas on how to fix it?

I had success by accessing the state. As far as I understand it it should be reactive as well this.$store.state.userData and reflect the current state of the store just as well.
Keep in mind accessing object properties might not be reactive the way you think they are: https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects
So you probably want to define a getter where you access the user's name, credentials, whatever userData holds.
The more explicit approach is defining every property of the userData object in the store and write a mutation for every property individually (loops are possible to do that at once, of course, but you'll have to create the mutators still).
{
state: {
userData: {
id: null,
name: '',
email: '',
// etc.
}
},
// ...
}
Doing it this way may also work successfully with the getter you defined.
For my work I would rather not trust handling whole objects at once. This is a bit of extra code to write but explicitly defining which properties the expected object should have is valuable to you and others.

Related

Vue JS : Method Style Getter (to filtering) AND reactivity to state change

I worked on a simple schedule application and i use Vuex as a state manager.
I want a way to filter the task state, to only return specific date and category tasks (Method Style Getter ig?) but i also want the component which get these tasks (via Method Style Getter) to react (so rerender) every time the task state change.
What is the most effective way to get there?
Access the tasks
export default {
name: "TasksContainer",
methods: {
logSomething() {
console.log("new task (good)");
},
},
components: {
Task,
},
computed: {
...mapGetters(["weekCourseTasks", "courseTasks", "nowMoment"]),
},
};
Update tasks
export default {
name: "AddTask",
data() {
return {
taskName: "",
taskDetail: "",
taskCourse: "",
taskDate: "",
};
},
methods: {
...mapActions(["addTask"]),
postForm() {
this.addTask({
taskName: this.taskName,
taskDetail: this.taskDetail,
taskCourse: this.taskCourse,
taskDate: this.taskDate,
});
this.$emit("new-task");
},
},
};
If I understand you correctly, you would like to have tasks, that you can filter based on the date and other categories.
Vuex allows for such getter methods.
The following code block allows you to write getters based on parameters.
getters: {
// ...
getTaskByDate: (state) => (date) => {
return state.tasks.find(task => task.date === date)
}
}
In order to see them:
store.getters.getTaskByDate(xyz);
This should allow you to filter your state and make the component responsive.
A full documentation can be found here under the part "Method-Style Access"
Additionally, be sure to perform your updates according to the guidelines to preserve reactivity.
Since a Vuex store's state is made reactive by Vue, when we mutate the
state, Vue components observing the state will update automatically.
This also means Vuex mutations are subject to the same reactivity
caveats when working with plain Vue:
Prefer initializing your store's initial state with all desired fields
upfront.
When adding new properties to an Object, you should either:
Use Vue.set(obj, 'newProp', 123), or
Replace that Object with a fresh one. For example, using the object
spread syntax we can write it like this:
state.obj = { ...state.obj, newProp: 123 }
(From the Vuex mutations documentation)

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 make data from localStorage reactive in Vue js

I am using localStorage as a data source in a Vue js project. I can read and write but cannot find a way to use it reactively. I need to refresh to see any changes I've made.
I'm using the data as props for multiple components, and when I write to localStorage from the components I trigger a forceUpdate on the main App.vue file using the updateData method.
Force update is not working here. Any ideas to accomplish this without a page refresh?
...............
data: function () {
return {
dataHasLoaded: false,
myData: '',
}
},
mounted() {
const localData = JSON.parse(localStorage.getItem('myData'));
const dataLength = Object.keys(localData).length > 0;
this.dataHasLoaded = dataLength;
this.myData = localData;
},
methods: {
updateData(checkData) {
this.$forceUpdate();
console.log('forceUpdate on App.vue')
},
},
...............
Here's how I solved this. Local storage just isn't reactive, but it is great for persisting state across refreshes.
What is great at being reactive are regular old data values, which can be initialized with localStorage values. Use a combination of a data values and local storage.
Let's say I was trying to see updates to a token I was keeping in localStorage as they happened, it could look like this:
const thing = new Vue({
data(){
return {
tokenValue: localStorage.getItem('id_token') || '',
userValue: JSON.parse(localStorage.getItem('user')) || {},
};
},
computed: {
token: {
get: function() {
return this.tokenValue;
},
set: function(id_token) {
this.tokenValue = id_token;
localStorage.setItem('id_token', id_token)
}
},
user: {
get: function() {
return this.userValue;
},
set: function(user) {
this.userValue = user;
localStorage.setItem('user', JSON.stringify(user))
}
}
}
});
The problem initially is I was trying to use localStorage.getItem() from my computed getters, but Vue just doesn't know about what's going on in local storage, and it's silly to focus on making it reactive when there's other options. The trick is to initially get from local storage, and continually update your local storage values as changes happen, but maintain a reactive value that Vue knows about.
For anyone facing the same dilemma, I wasn't able to solve it the way that I wanted but I found a way around it.
I originally loaded the data in localStorage to a value in the Parent's Data called myData.
Then I used myData in props to populate the data in components via props.
When I wanted to add new or edit data,
I pulled up a fresh copy of the localStorage,
added to it and saved it again,
at the same time I emit the updated copy of localStorage to myData in the parent,
which in turn updated all the data in the child components via the props.
This works well, making all the data update in real time from the one data source.
As items in localstorage may be updated by something else than the currently visible vue template, I wanted that updating function to emit a change, which vue can react to.
My localstorage.set there does this after updating the database:
window.dispatchEvent(new CustomEvent('storage-changed', {
detail: {
action: 'set',
key: key,
content: content
}
}));
and in mounted() I have a listener which updates forceRedraw, which - wait for it - force a redraw.
window.addEventListener('storage-changed', (data) => {
this.forceRedraw++;
...

How to update Vue component property when Vuex store state changes?

I'm building a simple presentation tool where I can create presentations, name them and add/remove slides with Vue js and Vuex to handle the app state. All is going great but now I'm trying to implement a feature that detects changes in the presentation (title changed or slide added/removed) and couldn't not yet find the right solution for it. I'll give the example only concerning the title change for the sake of simplicity. Right now in my Vuex store I have:
const state = {
presentations: handover.presentations, //array of objects that comes from the DB
currentPresentation: handover.presentations[0]
}
In my Presentation component I have:
export default {
template: '#presentation',
props: ['presentation'],
data: () => {
return {
shadowPresentation: ''
}
},
computed: {
isSelected () {
if (this.getSelectedPresentation !== null) {
return this.presentation === this.getSelectedPresentation
}
return false
},
hasChanged () {
if (this.shadowPresentation.title !== this.presentation.title) {
return true
}
return false
},
...mapGetters(['getSelectedPresentation'])
},
methods: mapActions({
selectPresentation: 'selectPresentation'
}),
created () {
const self = this
self.shadowPresentation = {
title: self.presentation.title,
slides: []
}
self.presentation.slides.forEach(item => {
self.shadowPresentation.slides.push(item)
})
}
}
What I've done so far is to create a shadow copy of my presentation when the component is created and then by the way of a computed property compare the properties that I'm interested in (in this case the title) and return true if anything is different. This works for detecting the changes but what I want to do is to be able to update the shadow presentation when the presentation is saved and so far I've failed to do it. Since the savePresentation action triggered in another component and I don't really know how pick the 'save' event inside presentation component I fail to update my shadow presentation. Any thoughts on how I could implement such feature? Any help would be very appreciated! Thanks in advance!
I ended up solving this problem in a different way than what I asked in the question but it may be of interest for some. So here it goes:
First I abdicated from having my vue store communicating an event to a component since when you use vuex you should have all your app state managed by the vuex store. What I did was to change the presentation object structure from
{
title: 'title',
slides: []
}
to something a little more complex, like this
{
states: [{
hash: md5(JSON.stringify(presentation)),
content: presentation
}],
statesAhead: [],
lastSaved: md5(JSON.stringify(presentation))
}
where presentation is the simple presentation object that I had at first. Now my new presentation object has a prop states where I will put all my presentation states and each of this states has an hash generated by the stringified simple presentation object and the actual simple presentation object. Like this I will for every change in the presention generate a new state with a different hash and then I can compare my current state hash with the last one that was saved. Whenever I save the presentation I update the lastSaved prop to the current state hash. With this structure I could simple implement undo/redo features just by unshifting/shifting states from states to statesAhead and vice-versa and that's even more than what I intended at first and in the end I kept all my state managed by the vuex store instead of fragmenting my state management and polluting components.
I hope it wasn't too much confusing and that someone finds this helpful.
Cheers
I had this issue when trying to add new properties to my user state so I ended up with this and it works well.
Action in Vuex store
updateUser (state, newObj) {
if (!state.user) {
state.user = {}
}
for (var propertyName in newObj) {
if (newObj.hasOwnProperty(propertyName)) {
//updates store state
Vue.set(state.user, propertyName, newObj[propertyName])
}
}
}
Implementation
Call your store action above from the Vue component
this.updateUser({emailVerified: true})
Object
{"user":{"emailVerified":true},"version":"1.0.0"}

Categories

Resources