I took the sample code from the documentation here and modified it a bit to use an object instead of an array for the todos field:
var todoStore = observable({
todos: { },
identity: function() { return this.todos; },
derived: function() {
let counter = 0; for(let key in this.todos) { ++counter; } return counter;
}
});
const print = function() {
console.log(
JSON.stringify(todoStore.todos),
JSON.stringify(todoStore.identity),
todoStore.derived
);
};
autorun(print);
// -> prints: { } { } 0
todoStore.todos.test = { title: "Take a walk", completed: false };
// -> should print: {"test":{"title":"Take a walk","completed":false}}
// {"test":{"title":"Take a walk","completed":false}} 1
todoStore.todos.test.completed = true;
// -> should print: {"test":{"title":"Take a walk","completed":true}}
// {"test":{"title":"Take a walk","completed":true}} 1
Now the whole automatic tracking doesn't work anymore. The places marked with -> should print: ... do _not_ print.
It I add another print(); call at the end of the sample code it prints this:
{"test":{"title":"Take a walk","completed":true}}
{"test":{"title":"Take a walk","completed":true}} 0
Note the wrong 0 at the end, since this should be 1!
Do I miss something here? Do I need to use mobservable for objects differently from how I would use it intuitively?
Hi,
Dynamically adding keys to objects is not picked up by mobservable (until ES6 proxies are generally available). But instead of that, you can use a observable map or extendObservable(todos, { test: .... })
OK, got it working by using map like this:
import * as mobs from 'mobservable';
var todoStore = mobs.observable({
todos: mobs.map(),
identity: function() { return this.todos; },
derived: function() { return this.todos.size; }
});
const print = function() {
console.log(
JSON.stringify(todoStore.todos.toJs()),
JSON.stringify(todoStore.identity.toJs()),
todoStore.derived
);
};
mobs.autorun(print);
todoStore.todos.set('test', { title: "Take a walk", completed: false });
todoStore.todos.get('test').completed = true;
I hoped for the nicer .prop access pattern just like for the built-in JavaScript objects.
I didn't managed it to get it going with extendObservable(). I tried it like this:
import * as mobs from 'mobservable';
var todoStore = mobs.observable({
todos: { },
identity: function() { return this.todos; },
derived: function() { return this.todos.size; }
});
const print = function() {
console.log(
JSON.stringify(mobs.toJSON(todoStore.todos)),
JSON.stringify(mobs.toJSON(todoStore.identity)),
todoStore.derived
);
};
mobs.autorun(print);
mobs.extendObservable(todoStore.todos, { test: { title: "Take a walk", completed: false } });
todoStore.todos.test.completed = true;
Could you give an advice about how to use extendObservable() correctly in this case?
The limitation with making plain objects observable is that the collection of keys cannot be observed (until ES6 proxies). So observable({ x: y}) or extendObservable(target, { x: y} ) will both introduce an observable _property_ x but since the keys collection is not observable your generic iteration of the object will not pick up the change.
In contrast, observable maps (and arrays) maintain an internal administration of the keys that exist in the object, so if you iterate on these, your autorun will retrigger as expected.
So isn't todoStore.todos marked as changed (markAtomChanged) when you extend it?
Sorry, but I still don't get it.
In the docs there is this statement for Object:
If a plain javascript object is passed to observable (that is, an object that wasn't created using a constructor function), Mobservable will recursively pass all its values through observable. This way the complete object (tree) is in-place instrumented to make it observable.
So, what does it mean for todoStore.todos? Are any changes to todoStore.todos observable at all? Esp. the mentioned recursively behavior made me thinking that todoStore.todos should be somehow observable, too.
And then later on in the same section:
- 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, unlessextendObservableis used.
This made me expecting that using extendObservable() would also trigger a change on todoStore.todos...
All the given code snippets (I have found so far) are just dealing with a one-level hierarchy and/or mostly with array-like structures; it would really help to have a real-world example with a deeper nesting level, since most apps are probably dealing with a non-flat state object (esp. if there is a single state object rooting all the app state).
Objects themselves are not observable. Only their individual properties are. When mobservable makes an object observable, it will only do so for the properties that are on the object on that moment. (and extendObservable can be used to add after-the-fact properties).
However, the set of keys, or the iterator, of an object is never observable (es5 simply hasn't have a feature for that). So if you have the following:
var obj = observable({ x: 1 });
autorun(() => {
for(var key in obj)
console.log(obj.x)
})
obj.x = 2; // will be picked up
obj.x = { y: 3 } // will be picked up, obj.x.y will be observable
obj.y = 3; // creates non observable property y, will not be picked up, although it will affect the iteration of autorun
obj.x = 5; // now y will be printed as well, but future changes to y will not be picked up, it isn't observable
extendObservable(obj, { z: 4 }) // won't be picked up; although the property is observable, the for loop won't detect the addition as the key set of an object isn't observable
obj.x = 6; // this causes a rerun of the autorun now z will be detected by the autorun.
obj.z = 5; // will be picked up; in the last iteration the autorun subscribed to obj.z as well
So it boils down to this: anything with a fixed set of properties (plain data object, classes, etc. etc) can be made observable by using observable / extendObservable.
If you want to vary on the keys that exist on an object itself, you need map if the keys are non-numeric, and arrays if the keys are numeric. With map you can even subscribe to not yet existing keys.
So in your case you need a map as you index your objects by some key. The objects inside that map are probably regular so observable(obj) will suffice for them (observable will be applied to any new values added to the map automatically by default)
OK, thanks, I think I understand the implications by now.
Maybe I can ask a followup question here with respect to: what are the observable methods of an observable array? I tried to use the sort() method directly within a view function, but that didn't work:
const arraySortBy = (array, prop) =>
array.sort((a, b) => a[prop].localeCompare(b[prop]));
const State = class {
@mobs.observable projectsList = [ ];
@mobs.observable getProjects() { return arraySortBy(this.projectsList, 'name'); };
};
I had to slice the array first like this:
const arraySortBy = (array, prop) =>
array.sort((a, b) => a[prop].localeCompare(b[prop]));
const State = class {
@mobs.observable projectsList = [ ];
@mobs.observable getProjects() { return arraySortBy(this.projectsList.slice(), 'name'); };
};
Is that a bug or is there only a subset of the native array operations observable? Would it be best to always use slice() on arrays in view functions?
And BTW, I first tried to _fix_ the first implementation by _referencing_ this.projectsList.length explicitly, but that was throwing an error about _cyclic dependencies_ at me. Not sure, what _cycle_ would be created by that...
any native methods is supported by mobservable, but sort is a nasty thing in javascript; it does not return the sorted array, but modifies the array in place! So i recommend using slice + lodash.sort which doesn't do so weird
Yeah, when I think about that it makes sense to only use _immutable_ methods on the array within view functions... would be a good addition for the docs... ;-)
And I am sorry to annoy you with all the nasty questions!
Definitely, I'll add it.
Btw, I was just thinking about this, would it make sense to make sort a pure function that always returns a sorted clone? This way it deviates from the javascript sort specs, but does it do the thing that everybody expects it to do; return a sorted copy when can easily be used as derivation.
Oh and don't worry about the questions! I understand that those limitations might be counter-intuitive, so @kmalakoff and / or I will make sure those concepts will be explained better in the next versions of the docs!
docs: sounds great about the planned improvementsarray.sort(): at least to me it would make sense to make sort() a pure function; actually I just realized that it is doing the sorting in-place when you mentioned it; but adding just a reminder about that behavior in the docs with an hint to .splice().sort() would also work...and just in case: I just found this somewhat related blog post: http://blog.lambda-it.ch/reactive-data-flow-in-angular-2/
Hi, i am new with mobx. I have one question that similar to this, how add obesrvable property with key from input?
extendObservable(target, { this.state.inputText : ''} )
doing this
target[this.state.inputText] = ' '
work but not on observable way.
@Shadowman4205 try this one:
extendObservable(target, { [this.state.inputText] : ''} )
Most helpful comment
Objects themselves are not observable. Only their individual properties are. When mobservable makes an object observable, it will only do so for the properties that are on the object on that moment. (and
extendObservablecan be used to add after-the-fact properties).However, the set of keys, or the iterator, of an object is never observable (es5 simply hasn't have a feature for that). So if you have the following:
So it boils down to this: anything with a fixed set of properties (plain data object, classes, etc. etc) can be made observable by using
observable/extendObservable.If you want to vary on the keys that exist on an object itself, you need
mapif the keys are non-numeric, and arrays if the keys are numeric. With map you can even subscribe to not yet existing keys.So in your case you need a map as you index your objects by some key. The objects inside that map are probably regular so
observable(obj)will suffice for them (observablewill be applied to any new values added to the map automatically by default)