MobX - Observable object grabbing only changed fields? - mobx

I have a MobX store setup with an observable object that has default values, and whose values gets populated from the server on load of react native scene. I have a list of observable user preferences in the UserPreferencesStore like this:
class UserPreferencesStore {
#observable userPreferences = {
receive_upvotes_mail: 0,
receive_answers_mail: 0,
receive_comments_mail: 0
}
}
On the RN side, these values change to this:
class UserPreferencesStore {
#observable userPreferences = {
receive_upvotes_mail: 1,
receive_answers_mail: 1,
receive_comments_mail: 0
}
}
I am not sure how to get only the changed items to send to the server. Any idea? Also, is this the most efficient way to use mobx for this situation, an observable object, even if I have 20 fields?

That should be a matter of establishing a separate autorun or reaction for each field:
class UserPreferencesStore {
#observable userPreferences = {
receive_upvotes_mail: 1,
receive_answers_mail: 1,
receive_comments_mail: 0
}
constructor() {
Object.keys(this.userPreferences).forEach(setting => {
reaction(
// whenever a new value is produced...
() => this.userPreferences[setting],
// ..run this effect
(newValue) => storeSettingOnServer(setting, newValue)
)
})
}
}

Related

Mobx observe only specific objects in array

I have a store with events
export class EventStore {
#observable
events: Event[] = [];
#action
addEvent(event: Event) {
this.events = [...this.events, event]
};
};
My Event look like this :
export class Event{
classType: string;
}
I want to observe change on events properties of store BUT only of a specific classType
For Eg :
I have Event with classType "AddToCart" and "Order", and I want to observe only "Order" added, removed from events
I want to use import { observer } from "mobx-react"
Question :
Is there some magic trick to do in the EventStore or I have to handle it in my component (with some if) ?
My unperfect solution
Meanwhile, I'm doing somehting like this :
reaction(
() => eventStore.lastEvent,
event => {
if (event.classType === "Order")
this.newEvent = { ...event }
}
)
You can add computed property
#computed
get orderEvents() {
// Add whatever filtering you want
return this.events.filter(event => event.classType === 'Order')
};
If you need to pass arguments you could you computedFn from mobx-utils:
filteredEvents = computedFn(function (classType) {
return this.events.filter(event => event.classType === classType)
})
Note: don't use arrow functions as the this would be incorrect.
https://github.com/mobxjs/mobx-utils#computedfn
https://mobx.js.org/refguide/computed-decorator.html#computeds-with-arguments

Can't get data of computed state from store - Vue

I'm learning Vue and have been struggling to get the data from a computed property. I am retrieving comments from the store and them processing through a function called chunkify() however I'm getting the following error.
Despite the comments being computed correctly.
What am I doing wrong here? Any help would be greatly appreciated.
Home.vue
export default {
name: 'Home',
computed: {
comments() {
return this.$store.state.comments
},
},
methods: {
init() {
const comments = this.chunkify(this.comments, 3);
comments[0] = this.chunkify(comments[0], 3);
comments[1] = this.chunkify(comments[1], 3);
comments[2] = this.chunkify(comments[2], 3);
console.log(comments)
},
chunkify(a, n) {
if (n < 2)
return [a];
const len = a.length;
const out = [];
let i = 0;
let size;
if (len % n === 0) {
size = Math.floor(len / n);
while (i < len) {
out.push(a.slice(i, i += size));
}
} else {
while (i < len) {
size = Math.ceil((len - i) / n--);
out.push(a.slice(i, i += size));
}
}
return out;
},
},
mounted() {
this.init()
}
}
Like I wrote in the comments, the OPs problem is that he's accessing a store property that is not available (probably waiting on an AJAX request to come in) when the component is mounted.
Instead of eagerly assuming the data is present when the component is mounted, I suggested that the store property be watched and this.init() called when the propery is loaded.
However, I think this may not be the right approach, since the watch method will be called every time the property changes, which is not semantic for the case of doing prep work on data. I can suggest two solutions that I think are more elegant.
1. Trigger an event when the data is loaded
It's easy to set up a global messaging bus in Vue (see, for example, this post).
Assuming that the property is being loaded in a Vuex action,the flow would be similar to:
{
...
actions: {
async comments() {
try {
await loadComments()
EventBus.trigger("comments:load:success")
} catch (e) {
EventBus.trigger("comments:load:error", e)
}
}
}
...
}
You can gripe a bit about reactivity and events going agains the reactive philosophy. But this may be an example of a case where events are just more semantic.
2. The reactive approach
I try to keep computation outside of my views. Instead of defining chunkify inside your component, you can instead tie that in to your store.
So, say that I have a JavaScrip module called store that exports the Vuex store. I would define chunkify as a named function in that module
function chunkify (a, n) {
...
}
(This can be defined at the bottom of the JS module, for readability, thanks to function hoisting.)
Then, in your store definition,
const store = new Vuex.Store({
state: { ... },
...
getters: {
chunkedComments (state) {
return function (chunks) {
if (state.comments)
return chunkify(state.comments, chunks);
return state.comments
}
}
}
...
})
In your component, the computed prop would now be
computed: {
comments() {
return this.$store.getters.chunkedComments(3);
},
}
Then the update cascase will flow from the getter, which will update when comments are retrieved, which will update the component's computed prop, which will update the ui.
Use getters, merge chuckify and init function inside the getter.And for computed comment function will return this.$store.getters.YOURFUNC (merge of chuckify and init function). do not add anything inside mounted.

vuex store not refresh computed property

Following the tutorial at this web address http://stackabuse.com/single-page-apps-with-vue-js-and-flask-state-management-with-vuex/, I encountered a problem that the function in the computed property was not automatically invoked after the state in the store was changed. The relevant code is listed as following:
Survey.vue
computed: {
surveyComplete() {
if (this.survey.questions) {
const numQuestions = this.survey.questions.length
const numCompleted = this.survey.questions.filter(q =>q.choice).length
return numQuestions === numCompleted
}
return false
},
survey() {
return this.$store.state.currentSurvey
},
selectedChoice: {
get() {
const question = this.survey.questions[this.currentQuestion]
return question.choice
},
set(value) {
const question = this.survey.questions[this.currentQuestion]
this.$store.commit('setChoice', { questionId: question.id, choice: value })
}
}
}
When a radio button in the survey questions is chosen, selectedChoice will change the state in the store. However surveyComplete method was not called simultaneously. What's the problem? Thanks in advance!
surveyComplete() method does not 'spy' your store, it will be updated, when you change this.survey.questions only. So if you modify the store, nothing will happen inside surveyComplete. You may use the store inside the method.

How to update an object in 'state' with react redux?

In my reducer, suppose originally I have this state:
{
"loading": false,
"data": {
"-L1LwSwW97KkwdSnYvsc": {
"likeCount": 10,
"liked": false, // I want to update this property
"commentCount": 5
},
"-L1EY2_fqzn7sM1Mbf_F": {
"likeCount": 8,
"liked": true,
"commentCount": 22
}
}
}
Now, I want to update liked property inside -L1LwSwW97KkwdSnYvsc object, which is inside data object and make it true. This is what I've been trying, but apparently, it's wrong, because after I've updated the state, the componentWillReceiveProps function inside a component that listens to the state change does not get triggered:
var { data } = state;
data['-L1LwSwW97KkwdSnYvsc'].liked = !data['-L1LwSwW97KkwdSnYvsc'].liked;
return { ...state, data };
Could you please specify why it's wrong and how I should change it to make it work?
You're mutating state! When you destructure:
var { data } = state;
It's the same as:
var data = state.data;
So when you do:
data[…].liked = !data[…].liked
You're still modifying state.data which is in turn mutating state. That's never good - use some nested spread syntax:
return {
...state,
data: {
...state.data,
'-L1LwSwW97KkwdSnYvsc': {
...state.data['-L1LwSwW97KkwdSnYvsc'],
liked: !state.data['-L1LwSwW97KkwdSnYvsc'].liked
}
}
};
Using spread operator is good until you start working with deeply nested state and/or arrays(remember spread operator does a shallow copy only).
I would rather recommend you starting working with immutability-helper instead. It is a React recommendation and it will let your code more readable and bug free.
Example:
import update from "immutability-helper";
(...)
const toggleLike = !state.data["-L1LwSwW97KkwdSnYvsc"].liked
return update(state, {
data: {
"-L1LwSwW97KkwdSnYvsc": {
like: {
$set: toggleLike
}
}
}
})

Aurelia observer not firing for array

I have a custom data grid element simplified like this:
export class DataGrid {
#bindable data;
dataChanged(newValue, oldValue) {
console.log("Sensing new data...", newValue);
}
}
It's instantiated like this:
<data-grid data.bind="records"></data-grid>
"Sensing new data..." and the array of records is displayed in the console when the data grid appears. However, when I delete a record from the array of objects, the dataChanged() function is not triggered.
let index = this.records.findIndex((r) => { return r.acc_id === this.record.acc_id; });
if (index > -1) {
console.log("Deleting element..." + index, this.records);
this.records.splice(index, 1);
}
I get "Deleting element..." in the console but not "Sensing new data...".
Any ideas why dataChanged() is not firing when I splice out a record?
You can not observe an Array for mutations like that. You have to use a collectionObserver instead. Right now, your dataChanged() would only fire if you overwrite the data value (ie data = [1, 2, 3] which overwrites it with a new array).
Example how to use the collectionObserver from the BindingEngine class, for your usecase:
import { BindingEngine } from 'aurelia-framework';
export class DataGrid {
static inject = [BindingEngine];
#bindable data;
constructor(bindingEngine) {
this._bindingEngine = bindingEngine;
}
attached() {
this._dataObserveSubscription = this._bindingEngine
.collectionObserver(this.data)
.subscribe(splices => this.dataArrayChanged(splices));
}
detached() {
// clean up this observer when the associated view is removed
this._dataObserveSubscription.dispose();
}
dataArrayChanged(splices) {
console.log('Array mutated', splices);
}
}