Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

How can I observe any change (property added, removed or changed) in a mobx observable map?

Ask Question let criteria = new FilterCriteria (); // setting up a reaction when something in the filter changes // (property added, removed, or changed) reaction(()=>criteria.filter, data => console.log(data.toJSON())); criteria.filter.set('name', 'John'); // setting a new property.

I would expect the above code to print out { 'name': 'John' } , but it seems that the reaction is not running.

I suspect that I set up the reaction in the wrong way. I want to react whenever a new key is added, an existing key is removed or a key value is changed. I don't know the keys or values at compile time.

How am I supposed to do that?

UPDATE

I changed my code to

class FilterCriteria {
    @observable filter = new Map();
    @computed get json(){ return this.filter.toJSON(); }
reaction(()=>criteria.json, data => console.log(data));

and now it seems to work properly. The reaction sideffect is executed whenever I add, remove or change a value in the Map.

So the question is why the reaction did execute in the second but not in the first example?

UPDATE 2

I changed my code again for a second time. I reverted to almost the first version but this time instead of reacting on criteria.filter and logging data.toJSON(), i react on criteria.filter.toJSON() and I log data (toJSON is moved from the sideffect to the value being watched). This time the reaction runs normally.

class FilterCriteria {
    @observable filter = new Map();
reaction(()=>criteria.filter.toJSON(), data => console.log(data));

Again, I don't understand why. If criteria.filter is not an observable in itself then how does the watched expression is reevaluated when something inside criteria.filter is changed?

UPDATE 4 (hope the final one) SOLUTION

According to MobX documentation, mobx reacts to any existing observable property that is read during the execution of a tracked function.

reaction side-effect executes when the observable property changes. In my example, when reacting to criteria.filter , the observable property that is read here is filter, but the filter itself never changes. It is the same map always. It is the properties of filter that change. So the reaction is never run for criteria.filter.

But when I react on criteria.filter.toJSON() or mobx.toJS(criteria.filter), the reaction is executed correctly.

So why is that? criteria.filter doesn't change, and toJSON is not an observable property. It is a function. same for mobx.toJS. It seems no properties are read here. But this is not correct. As the documentation states (but not so emphatically), the properties of criteria.filter are indeed read when toJSON or mobx.toJS is executed, because both functions create a deep clone of the map (thus iterating over every property).

Now, in the beginning, the Map did not contain any property. So how is it that newly added properties are tracked, since they did not exist (to be read) when tracking begun? This is a map's feature. Maps provide observability for not yet existing properties too.

In MobX 5 you can track not existing properties of observable objects (not class instances) too, provided that they were instatiated with observable or observable.object. Class instances don't support this.

I'd like to know the answer to "how do I do this properly?" rather than "why doesn't it work?" I came here already knowing why it doesn't work, but I'm leaving still not knowing how to do it properly. (toJSON() works but is very inefficient if the map is large; in my case, all I want to do is set a boolean flag when something changes, and it's pretty silly to convert the whole thing to JSON just for that.) – Qwertie Jan 31 at 19:06

In mobx you have two options when you want to observe changes to something that is observable. reaction and observe. Reaction allows you to specify when you want some function to be called when a specific aspect of the observable changes. This could be changes to an array length, keys, properties, really anything. observe will trigger some function any time that the observable has changed.

I suspect the reason that your reaction hasn't been triggered is because of the first function. () => criteria.filter. This will not be triggered when a key is added/removed or a value changed. Instead, it will be triggered when filter actually changes. And since filter is really a reference to the Map, it will never change, even when the Map itself changes.

Here are some examples to illustrate my point:

If you want to trigger a reaction when a key has been added or removed, you may want your function to be:

() => criteria.filter.keys()

The result of this function will be different when a key has been added or removed. Similarly, if you want to trigger a reaction for when a value has been modified, something like this should work:

() => criteria.filter.values()

So some combination of those two should be what you need to listen to changes to keys/values. Alternatively, you could use observe, which will trigger on every change and require you to check what has changed to ensure that your specific conditions have been met to warrant calling a function (ie. key/value change)

UPDATE: Here is an example that illustrates the problem

@observable map = new Map();

Lets say that the value of map in memory is 5. So when you check map === map, it is equivalent to 5 === 5 and will evaluate to true.

Now, looking at the first code snippet you posted:

reaction(() => map, data => console.log(map.toJSON()));

Every time you add/remove a key or change a value, that first function will run. And the result will be 5, since that is what we said the value in memory is for this example. It will say: the old value is 5, and the new value is 5, so there is no change. Therefore, the reaction will not run the second function.

Now the second snippet:

reaction(() => map.toJSON(), data => console.log(data));

At first the result of the function will be: {} because the Map is empty. Now lets add a key:

map.set(1, 'some value');

Now, the result of the first function will be:

{"1": "some value"}

Clearly, this value is different than {}, so something has changed, and the second function of the reaction is called.

I suspected that this was the reason. So I created a computed @computed get json() { return this.filter.toJSON(); } and I reacted to criteria.json instead of criteria.filter. And it worked. But I don't understand why. If the computed depends on the criteria.filter, and the reaction reacts on the computed, why didn't the reaction react on the critieria.filter itself? – Thanasis Ioannidis Feb 6, 2019 at 20:19 @ThanasisIoannidis you have to remember that filter is a reference/pointer to the Map object, and the pointer will not change over the lifetime of the Map, unless of course you reassign it to another Map. The computed JSON property worked because the result is a string, and the comparison of strings is different – Wolfie Feb 6, 2019 at 20:43 I know what you said about filter as a pointer. Totally understand it. I changed again the code. I removed the computed property and I reverted to the first example with one difference. Instead of reacting on filter and printinf filter.toJSON, I reacted to filter.toJSON and printing the result. And this time it workes. As if filter is not observed but filter.toJSON is. Also it reacts on mobx.toJS(filter). In this last case I don't have any computed. I have the filter that is run through a utility library. And still it reacts. it won't react on filter alone though. – Thanasis Ioannidis Feb 6, 2019 at 21:13 yes I understand the concept of pointers and references and that the map itself doesn't change. I have also put updates in my question. The 4th update is also the answer to my question. I had to carefully read mobx documentation. According to it, mobx reacts to the change of "any observable property that is read during the execution of a tracked function". It doesn't say that mobx reacts to the change of the result of a tracked function. In order for sideeffect to execute there must be at least one observable property that is read in the first argument of reaction. – Thanasis Ioannidis Feb 8, 2019 at 10:22 So "criteria.filter.toJSON()" doesn't appear to have any observable property that is read here (and it's value change). So how does mobx react? You say it does because the resulting JSON changes. But this is what I ask. How on earth mobx knows that the resulting JSON has changed, in order to trigger the reaction? In order to know that the JSON has changed it must observe something in the Map, that when that something changes, mobx would now that it will result in a different JSON and so it would execute the tracking function again and fire the sideffect. – Thanasis Ioannidis Feb 8, 2019 at 10:28

Thanks for contributing an answer to Stack Overflow!

  • Please be sure to answer the question. Provide details and share your research!

But avoid

  • Asking for help, clarification, or responding to other answers.
  • Making statements based on opinion; back them up with references or personal experience.

To learn more, see our tips on writing great answers.