How to make vuex getters reactive - javascript

I working on a schedule with VUE, I'm really new with VUE and with JS. I created VUE component that adds some new properties into the object which already in state and then I take this object with getters and render inside my another VUE component, but unfortunately new properties render only after reloading.
Here is snippet from my component where I add new property
methods: {
...mapActions({addDateToState: 'addDate'}),
addDate () {
this.date = this.startTime.time;
//date from datepicker
this.addDateToState({date:this.date, year:this.year, time:this.event.time, name:this.event});
// object need to add
},
}
Here is a snippet from state
const state = {
schedule: {}
}
Here is actions
addDate ({commit}, date) {
commit('ADD_DATE', date)
}
And here is the mutation which does all the work
ADD_DATE (state, date) {
if (typeof state.schedule[date.year] === 'undefined') {
state.schedule[date.year] = {};
}
if (typeof state.schedule[date.year][date.date] === 'undefined') {
state.schedule[date.year][date.date] = {};
}
if (typeof state.schedule[date.year][date.date][date.time] === 'undefined') {
state.schedule[date.year][date.date][date.time] = [];
}
state.schedule[date.year][date.date][date.time].push(date.name)
//interesting that this properties are reactive, so I see chenges when pushh new element to the array, but don't see if it is new property on the object
console.log(state.schedule)
//here I see the schedule which already has new properties so mutation works
}
Getters
const getters = {
schedule() {
return state.schedule;
}
}
And here is computed property which get the schedule from getters
computed: {
...mapGetters([
'schedule'
])
}
So the problem that I can't make this getter reactive, but I see that state was changed. Can someone help me?

You need to use Vue.set/this.$set if you are adding new property. See vue docs

me, when i need a reactive getter in vue a do that:
computed: {
getSchedule: function () { return this.schedule; }
}

Related

Vue 2: Set empty class as data property

I have a scenario in Vue 2 where I am initializing API classes as data properties on my component like so:
new Vue({
el: '#my-element'
data: {
apiUrl: apiUrl,
api: new ApiClient(this.apiUrl)
}
})
API Client:
class ApiClient {
constructor(apiUrl) {
this.apiUrl = apiUrl;
}
async getRequest() {
// Perform GET
}
}
This works just fine, but I have scenarios where I need to use two different API clients. If a certain prop gets passed into my component, I want to initialize that data property as secondApi, as an example.
In this scenario, I see myself using the created() hook:
created() {
if (prop === true) {
this.secondApi = new SecondApiClient(this.apiUrl);
}
}
In my data : { } object though, I'm not sure how to initialize this optional secondApi property properly (in the case that the prop is not passed in). Do I just set it as an empty object at first? It will always be a class object like apiClient. Is there an empty class data type?
Yes you can follow one of the below
Approach 1:
data() {
return {
apiUrl: apiUrl,
api: null // or you can even keep it as new ApiClient(this.apiUrl) if that's the default value
}
},
props: ['certainProperty'],
created() {
if (this.certainProperty === true) { // certainProperty is a prop
this.api = new SecondApiClient(this.apiUrl);
} else this.api = new ApiClient(this.apiUrl);
}
Sometimes there might be a delay in receiving the props so its better to follow the below approach
Approach 2:
data() {
return {
apiUrl: apiUrl,
api: null // or you can even keep it as new ApiClient(this.apiUrl) if that's the default value
}
},
props: ['certainProperty'],
watch: {
certainProperty(newVal) {
if (newVal === true) { // certainProperty is a prop
this.api = new SecondApiClient(this.apiUrl);
} else this.api = new ApiClient(this.apiUrl);
}
}
Note: You need to pass the props from parent component like
<child-component :certainProperty="true" />

Why can't I assign a property to an object using bracket notation + a variable?

I'm very new to Vuex, and trying to assign a value to a Vuex state, (state.map.status.isReady for this one).
However, I want to make my code reusable, so I created a function changeMapStatus(state, key, value) in order to achieve that.
This function modifies the property state.map.status.key to value it received.
However, when I call the mutation with this.$store.commit('changeMapStatus', 'isReady', true) from a component file, it simply removes the state.map.status.isReady and that property becomes undefined.
On the another hand, if I change the function to
changeMapStatus(state, value) {
state.map.status.isReady = value;
}
it somehow works.
Can you help me which point I get it wrong?
Thanks so much!
store.js (Vuex)
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
map: {
status: {
isReady: false,
},
},
},
mutations: {
changeMapStatus(state, key, value) {
state.map.status[key] = value;
}
},
});
According to Vuex official docs, mutation takes 2 parameters state and payload. You can use the spread operator to get values from the payload.
changeMapStatus(state, {key, value}) {
state.map.status[key] = value;
}
this.$store.commit('changeMapStatus', {key: 'isReady', value: true})
Or else you can use it like this
changeMapStatus(state, payload) {
state.map.status = {
...state.map.status,
...payload,
}
}
this.$store.commit('changeMapStatus', { isReady: true });
You could pass an object as parameter that contains key and value as follows :
changeMapStatus(state, myObj) {
state.map.status[myObj.key] = myObj.value;
}
and call it like:
this.$store.commit('changeMapStatus', {key:'isReady', value:true})

React to nested state change in Angular and NgRx

Please consider the example below
// Example state
let exampleState = {
counter: 0;
modules: {
authentication: Object,
geotools: Object
};
};
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
}
Here in the MyAppComponent we react on changes that occur to the counter property of the state. But what if we want to react on nested properties of the state, for example modules.geotools? Seems like there should be a possibility to call a store.select('modules.geotools'), as putting everything on the first level of the global state seems not to be good for overall state structure.
Update
The answer by #cartant is surely correct, but the NgRx version that is used in the Angular 5 requires a little bit different way of state querying. The idea is that we can not just provide the key to the store.select() call, we need to provide a function that returns the specific state branch. Let us call it the stateGetter and write it to accept any number of arguments (i.e. depth of querying).
// The stateGetter implementation
const getUnderlyingProperty = (currentStateLevel, properties: Array<any>) => {
if (properties.length === 0) {
throw 'Unable to get the underlying property';
} else if (properties.length === 1) {
const key = properties.shift();
return currentStateLevel[key];
} else {
const key = properties.shift();
return getUnderlyingProperty(currentStateLevel[key], properties);
}
}
export const stateGetter = (...args) => {
return (state: AppState) => {
let argsCopy = args.slice();
return getUnderlyingProperty(state['state'], argsCopy);
};
};
// Using the stateGetter
...
store.select(storeGetter('root', 'bigbranch', 'mediumbranch', 'smallbranch', 'leaf')).subscribe(data => {});
...
select takes nested keys as separate strings, so your select call should be:
store.select('modules', 'geotools')

How to track nested object in a MobX store

Let's stay I have this myObject loaded via an API call:
myObject = {
fieldA: { details: 'OK', message: 'HELLO' },
fieldB: { details: 'NOT_OK', message: 'ERROR' },
}
Only details and message of each field can change. I want this object to be observable in a MobX store (which properties? to be defined below). I have a simple React component which reads the two fields from the store:
#observer
class App extends Component {
store = new Store();
componentWillMount() {
this.store.load();
}
render() {
return (
<div>
{this.store.fieldA && <p>{this.store.fieldA.details}</p>}
{this.store.fieldB && <p>{this.store.fieldB.details}</p>}
</div>
);
}
}
I read this page trying to understand what MobX reacts to, but still didn't get a clear idea. Specifically, which of the 4 stores below would work, and why?
1/
class Store1 = {
#observable myObject = {};
#action setMyObject = object => {
this.myObject = object;
}
load = () => someAsyncStuff().then(this.setMyObject);
}
2/
class Store2 = {
#observable myObject = {};
#action setMyObject = object => {
this.myObject.fieldA = object.fieldA;
this.myObject.fieldB = object.fieldB;
}
load = () => someAsyncStuff().then(this.setMyObject);
}
3/
class Store3 = {
#observable myObject = { fieldA: {}, fieldB: {} };
#action setMyObject = object => {
this.myObject = object;
}
load = () => someAsyncStuff().then(this.setMyObject);
}
4/
class Store4 = {
#observable myObject = { fieldA: {}, fieldB: {} };
#action setMyObject = object => {
this.myObject.fieldA = object.fieldA;
this.myObject.fieldB = object.fieldB;
}
load = () => someAsyncStuff().then(this.setMyObject);
}
All of the above will work, except for solution 2.
That because as described in Mobx docs about objects:
When passing objects through observable, only the properties that
exist at the time of making the object observable will be observable.
Properties that are added to the object at a later time won't become
observable, unless extendObservable is used.
In the first solution you re-assign the object again with the properties already exist in the returned object.
In 3 and 4 you initialized the object with those 2 properties so it works.
Also I think in your component example you meant to use it like this (otherwise, it won't work in any way):
render() {
const { myObject } = this.store;
return (
<div>
{myObject && myObject.fieldA && <p>{myObject.fieldA.details}</p>}
{myObject && myObject.fieldB && <p>{myObject.fieldB.details}</p>}
</div>
);
}

VueJS observe plugged parameter

I'm starting with VueJS 2 and I created a simple plugin which adds parameter to Vue instance.
I have problem because when I update this value my computed properties are still same.
My example plugin's code:
export default function (Vue) {
Vue.MyProperty = "test"
Object.defineProperties(Vue.prototype, {
"$myProperty": {
"get": function () {
return Vue.MyProperty
},
"set": function (value) {
Vue.MyProperty = value
return this
}
}
})
}
And my component's code
export default {
"computed": {
"test": function () {
return this.$myProperty
}
}
}
When I changed this.$myProperty in other component my component returns vaid value (in example when I changed from "test" into "newvalue" I can see "newvalue") but computed property test is still old value ("test" in my example).
I tried to use this.$set(this, "$myProperty", value) but this still not working.
How can I use or declare this property to use it in computed or watched properties?
The reason the data value is not automatically updated in the computed is because the property you added to Vue, MyProperty is not an observed property. Fundamentally, Vue's reactivity works because all values added to data are converted into observed properties; under the hood they are converted into getter/setter pairs with some additional code so that when one of those properties changes, Vue knows to propagate the changes to all the things that depend on it's value.
The code in the question, however, just adds a normal property to the Vue object. You can change it, but it's not reactive.
That said, it's relatively easy to make it reactive. I cover how to do this in the comments to my answer here. Basically, instead of adding your property to Vue, just create a new Vue object (which has very low overhead) and make the property you want to be reactive a property of that Vue. Here is a working example.
console.clear()
function MyPlugin(Vue) {
let store = new Vue({data:{MyProperty: "some value"}})
Object.defineProperties(Vue.prototype, {
"$myProperty": {
"get": function () {
return store.MyProperty
},
"set": function (value) {
store.MyProperty = value
return this
}
}
})
}
Vue.use(MyPlugin)
const MyComponent = {
template:`<div>{{test}}</div>`,
"computed": {
"test": function () {
return this.$myProperty
}
}
}
new Vue({
el: "#app",
components:{
MyComponent
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<my-component></my-component>
<button #click="$myProperty = 'new value'">Change</button>
</div>

Categories

Resources