11

I need to replace data in my observable object when I get a new dump from the socket:

class Store {
    @observable data = { foo: 'bar' }
    replaceFromDump(newData) {
        this.data = newData
    }
}
const store = new Store()
store.replaceFromDump({ foo: 'bar' })

// { foo: 'bar' } can be a huge amount of JSON

However, I noticed performance hits when the data object scales, probably because MobX will trigger reactions everywhere even if some properties/values are identical.

Is there a "smarter" way? - I’m thinking that f.ex only replacing the affected parts of the object would be better than replacing the entire observable?

I made a small demo here explaining what I mean: https://jsfiddle.net/yqqxokme/. Replacing the object causes new reactions, even if the data is exactly the same (expected). But I’m sure there is a way to only mutate the affected parts of the data object like in the merge() function.

David Hellsing
  • 106,495
  • 44
  • 176
  • 212
  • Have you tried [extendObservable](https://jsfiddle.net/yqqxokme/3/)? – Tholle Apr 25 '18 at 13:17
  • As the data object itself ever modified itself in the client, or is it always used / replaced as a whole? In the latter case you can use `@observable.ref` which avoid creating observables recursively, see also the docs on modifiers – mweststrate Apr 26 '18 at 08:22
  • @mweststrate data comes as a dump only the first time and on reconnect, then in small chunks during the application lifecycle that we merge into the existing data object. But the performance hit is only noticeable when injecting a new dump over an existing (outdated) data observable – David Hellsing Apr 26 '18 at 11:18
  • Does this help? https://github.com/mobxjs/mobx-state-tree – Tarun Lalwani Apr 26 '18 at 12:53
  • @David, will appreciate your feedback on the answer as the bounty is about to expire – Tarun Lalwani May 03 '18 at 09:58

4 Answers4

6

So here are few things and cases. I have changed the dump function to below to simulate changes

variations = [
  {foo: 'bar'},
  {foo: 'bar'},
  {foo: 'bar2' },
  {foo: 'bar2' },
  {foo: 'bar2', bar: {name: "zoo"} },
  {foo: 'bar2', bar: {name: "zoo"} },
  {foo: 'bar2', bar: {name: "zoo2"} },
  {foo: 'bar2', bar: {name: "zoo2"} },
  {foo: 'barnew', bar: {name: "zoo2", new: "yes"} },
  {foo: 'barnew', bar: {name: "zoo2", new: "no"} },
  {foo: 'barnew', bar: {name: "zoo2", new: "no"} }
]

i=0;

dump = () => {
  i++;
  i = i%variations.length;
  console.log("Changing data to ", variations[i]);
    store.replaceFromDump(variations[i])
}

Using extendObservable

Now if you use below code

replaceFromDump(newData) {
  extendObservable(this.data, newData)
}

And run it through the dump cycle, the output is below

Output 1

The event for bar won't start raising until you get a change to foo, which happens on below change

{foo: 'barnew', bar: {name: "zoo2", new: "yes"} },

Outcome: New keys can only be observed existing observable keys change

Using map

In this we change the code like below

  @observable data = map({
    foo: 'bar'
  })

replaceFromDump(newData) {
  this.data.merge(newData)
}

Output 2

Outcome: The data is merge only and won't get deletions. You also will get duplicate events as it is a merge only option

Using Object Diff

You can use an object diff library like below

https://github.com/flitbit/diff

You can update the code like below

  @observable data = {
    foo: 'bar'
  }

replaceFromDump(newData) {
    if (diff(mobx.toJSON(this.data), newData)){
        this.data = newData;
    } 
}

Output 3

Outcome: The events only happen when data change and not on re-assignment to same object

Using Diff and Applying Diff

Using the same library we gave used earlier, we can apply just the changes needed

If we change the code like below

replaceFromDump(newData) {
    observableDiff(toJSON(this.data), newData, d => {
          applyChange(this.data, newData, d);
    })
  } 

If run the above, we get following output

Output 4

Outcome: Only changes to initial set of keys is observed, give you don't delete those in keys in between

It also gives you diff in below format

{"kind":"E","path":["foo"],"lhs":"bar2","rhs":"barnew"}
{"kind":"N","path":["bar","new"],"rhs":"yes"}

Which means you can have better control of things based on field names when you want

Below is the fiddle that I used, most code commented but in case you need to look at the imports use below

https://jsfiddle.net/tarunlalwani/fztkezab/1/

Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
1

You can read about optimizing your components: https://mobx.js.org/best/react-performance.html

On default Mobx triggers only the components that use the state in their render function. Thus not all components get triggered to render.

React renders all child components that use props that have changed.

That said, the more state you change the more you have to re-render. Thus i'd advise to only sync changes and use the @action decorator to make sure rendering is only done once and not on every change made on state.

@observable data = {}

@action
replaceChanges(partialNewData) {
    Object.keys(partialNewData).forEach(propName => {
       this.data[propName] = partialNewData[propname];
   }
}

Mobx doesnt check if changed state is actual the same. thus even changing the state with the same object can trigger a re-render. (https://mobx.js.org/best/react.html)

Yes as you state: you could also deep merge/overwrite the new state over the old state only for properties that have changed. this would also trigger less re-renders.

If you write your code properly (eg: don't use labmda statements in your react render method), your code should re-render pretty efficiently.

Joel Harkes
  • 10,975
  • 3
  • 46
  • 65
  • lambda statements are not slow (if you don't have 1000 in one render method) – uladzimir Apr 24 '18 at 13:10
  • 1
    @havenchyk they are not slow but they do trigger re-renders (in subcomponents) because they are re-created on every render method. – Joel Harkes Apr 24 '18 at 13:49
  • that's right, but I'd recommend author to measure first – uladzimir Apr 24 '18 at 14:13
  • Thanks, I know about optimizing react components, but this is more of a MobX use case. See this demo what I mean about replace vs modify: https://jsfiddle.net/yqqxokme/ – David Hellsing Apr 24 '18 at 16:11
  • Setting it to another string works, aparently it sees if the string equals and wont trigger. but replacing a whole object, the memory reference changes and thus always triggers even if the object is the same: https://jsfiddle.net/yqqxokme/1/ – Joel Harkes Apr 25 '18 at 08:23
  • This might also be helpful: https://mobx.js.org/best/react.html#mobx-tracks-property-access-not-values – Matt Browne Apr 25 '18 at 11:36
  • @MattBrowne thanks, there is already a link of this just below the code example ;-) – Joel Harkes Apr 25 '18 at 14:16
  • Oh, not sure how I missed that, haha – Matt Browne Apr 25 '18 at 19:28
1

Using extendObservable will prevent reactions from firing if the values are identical:

class Store {
    @observable data = { foo: 'bar' }
    replaceFromDump(newData) {
        extendObservable(this.data, newData)
    }
}
const store = new Store()
store.replaceFromDump({ foo: 'bar' })
Tholle
  • 108,070
  • 19
  • 198
  • 189
0

You are always creating a new Object on this line and passing it to function

store.replaceFromDump({ foo: 'bar' })

Two Object's even with same key/value pairs will not return true from this kind of a if-statement

if( object1 === object2 )

So this feature is working as intended and as it should when taken this in though.

You could check yourself that if data has changed this way.

  replaceFromDump(newData) {
    if( JSON.stringify( this.data ) !== JSON.stringify( newData ) ){
        this.data = newData
    }
  }

I would then use React.PureComponent's for your classes you do rendering in which should help you to get rid of extra/non-intented render's. So just extend your classes from them

class IRenderMobX extends React.PureComponent

React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.

If your React component’s render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.

In my opinion I don't think you achieve any remarkable performance benefits of implementing merge() kind of mutation behaviour. Your PureComponent's should be able to know themselves if they should re-render or not by doing shallow prop and state comparison and this should not be yours problem to worry about.

Community
  • 1
  • 1
Jimi Pajala
  • 2,358
  • 11
  • 20