Mobx: No way to observe property changes of objects in observable array?

Created on 15 Sep 2017  Â·  7Comments  Â·  Source: mobxjs/mobx

I've been struggling with this for quite some time and many searches/threads have come up short.

I need to observe specific property changes on items inside an array and so something with the object that changed. Is this not possible with Mobx?

Example:

const {observable, computed, autorun, observe, reaction} = mobx;

class Store {
    @observable items = [];
}

const store = new Store();

for(var i = 0; i < 10; i++)
    store.items.push({foo: 'cat', bar: 'dog'})


// Doesn't trigger on property change
observe(store.items, (change) => {
    console.log('observe(store.items) change = ', change);
});

// throws error: 'It is not possible to get index atoms from arrays'
/*
observe(store.items, 'foo', (change) => {
    console.log('observe(store.items) change = ', change);
});
*/


// reacts when item.foo changes, but no way to get the `item` of the change?
reaction(() => store.items.map(item => item.foo), function(map, reaction) {
    console.log('reaction(...) ');
})


// only want to observe 'foo' property on each item 
store.items[5].foo = 'monkey';

// NOT any other key 
store.items[5].bar = 'test';

With Ractive.js I could simply do this.observe('store.items.*.foo', (item) => { ... }). And my callback would be called anytime a foo key was changed on any object inside store.items and return the item that was changed. Is this possible with MobX? What's a workaround?

Most helpful comment

In Mobx most people make item responsible for listening to these kind of chagnes, e.g.

class Item {
  @observable foo

  constructor() {
     reaction(() => this.foo, newValue => { doStuff() })
  }
}

note that with mobx-state-tree you have a little more flexibility in this regard as you could listen to the patch stream as well, so you would know which item has changed.

note that even the following would be possible, but I think it will be quickly confusing. (and to make it efficient you probably don't want to clean / recreate all the disposers all the time, depending on the problem size)

let disposers = []
reaction(store.items, items => {
  disposers.forEach(f => f()) // clean up old disposers
  disposers = items.map(item => reaction(() => item.foo, () => item.doStuff()))
}

But, as @urugator said, what is the problem you are trying to solve in the first place? As people tend to overuse reactions, where computed values would be a much more elegant approach anyway :)

All 7 comments

Short answer, it's not easily achievable with MobX.
May I ask what is the original problem you're trying to solve? Maybe it can be approached a bit differently. What should happen upon the change?

In Mobx most people make item responsible for listening to these kind of chagnes, e.g.

class Item {
  @observable foo

  constructor() {
     reaction(() => this.foo, newValue => { doStuff() })
  }
}

note that with mobx-state-tree you have a little more flexibility in this regard as you could listen to the patch stream as well, so you would know which item has changed.

note that even the following would be possible, but I think it will be quickly confusing. (and to make it efficient you probably don't want to clean / recreate all the disposers all the time, depending on the problem size)

let disposers = []
reaction(store.items, items => {
  disposers.forEach(f => f()) // clean up old disposers
  disposers = items.map(item => reaction(() => item.foo, () => item.doStuff()))
}

But, as @urugator said, what is the problem you are trying to solve in the first place? As people tend to overuse reactions, where computed values would be a much more elegant approach anyway :)

@urugator @mweststrate Thank you both. I think I get it: Instead of observing pojo objects, I should observe properties on class instances that can then react to changes within their own domain.

As for my use case, I have a large set of addresses that need to be geocoded and mapped if one of them change. So if a single address changes in the set, I want to geocode only that one and react to any other upstream change from there. Currently, it is geocoding every single address again which is not needed and a lot of superfluous API calls! I think refactoring to a model structure would solve it though.

Closing since there's no real issue here. Thanks again!

Sorry to necro this thread, but I thought it was kind of funny that nearly exactly three years later I came across this thread again while googling for a way to do this for a completely unrelated use case. Then I realized that it was my own thread 😅

Point being, I guess this is a pretty common problem.

In case you're curious, this time my use case is that I have an array of classes with observable properties in a form (exactly like the suggestions given 3 years ago). The form is preloaded with these classes from the server and allows the user to add/remove them and change the properties of each one. I was looking for a simple way to set a "dirty" flag on the form when the user changes any of the properties on any of the classes that constitute the form (so, observe items[].*)

class Item { 
   @observable
   name: string

    ....
    (50+ other properties)

}

class FormStore {
  @observable
  items: Item[] = []
  @observable dirty = false
  constructor() {
     // I wish this worked on property changes too :(
     observe(items, () => {
       this.dirty = true
     })
  }
}

I guess the only way to do this is to setup/destroy a reaction on all the properties of each Item in Items whenever an Item is removed/added? Really wish there was a more simple way to property changes to arrays of observable objects.

Check mobx-utils.deepObserve

On Tue, 8 Sep 2020, 23:10 Jonathan Dumaine, notifications@github.com
wrote:

In case you're curious, this time my use case is that I have an array of
classes with observable properties in a form (exactly like the suggestions
given 3 years ago). The form is preloaded with these classes from the
server and allows the user to add/remove them and change the properties of
each one. I was looking for a simple way to set a "dirty" flag on the form
when the user changes any of the properties on any of the classes that
constitute the form (so, observe items[].*)

class Item {
@observable
name: string

....
(50+ other properties)

}

class FormStore {
@observable
items: Item[] = []
constructor() {
// I wish this worked on property changes too :(
observe(items, () => {
this.dirty = true
})
}
}

I guess the only way to do this is to setup/destroy a reaction on all the
properties of each Item in Items whenever an Item is removed/added?
Really wish there was a more simple way to property changes to arrays of
observable objects.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/1167#issuecomment-689163024, or
unsubscribe
https://github.com/notifications/unsubscribe-auth/AAN4NBDCXJCBS76VLDESCTLSE2TUZANCNFSM4D3FY7VA
.

What exactly are looking in deepObserve? Isn't it essentially still looking for changes to that array rather then the nested object hence still needing to destruct/reconstruct?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Kukkimonsuta picture Kukkimonsuta  Â·  65Comments

arackaf picture arackaf  Â·  29Comments

mweststrate picture mweststrate  Â·  75Comments

FouMez picture FouMez  Â·  31Comments

bySabi picture bySabi  Â·  95Comments