Replies: 10 comments 5 replies
-
You're just confused by const o = {};
console.log(o);
o.a = 5;
// inspecting `o` in dev tools displays `{ o: 5 }` So in some sense the autorun would have to see in the future, which would be quite cool, but we are not there yet. The
Could be the case, not sure atm. I think the reasoning could be that observing |
Beta Was this translation helpful? Give feedback.
-
I'm not confused about how console.log behaves. This was run on the command line, not browser dev tools. However, I'm getting the same results now between the use of Here was my particular problem that initiated this let observed;
observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed.length)
})
// triggers
action(() => observed.push('bill'))()
// triggers
action(() => observed[1] = 'moo')() output
Part of the object of mobx is to reduce re-rendering. Here we see that the autorun that is observing This is problematic because using let observed;
observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed)
})
// does not trigger
action(() => observed.push('bill'))() This can be resolved by using computed, but I've never seen this in the wild: let observed;
observed = observable(['moo', 'bob'])
let computed_length = computed(()=> {
console.log('computed')
return observed.length})
autorun(() => {
console.log("Ran", computed_length.get())
})
// does trigger
action(() => observed.push('bill'))()
// does not trigger
action(() => observed[1] = 'moo')() output
As I also pointed out in my example, this cross-property observation has further problems than just let observed;
observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed[0])
})
// triggers
action(() => observed.push('bill'))()
// triggers
action(() => observed[1] = 'moo')() Output
Here, in attempting to observe just |
Beta Was this translation helpful? Give feedback.
-
This was a very deliberate performance tradeoff, most observers do
something for an entire collection, and the current approach makes it much
cheaper to track loops / maps / filters, or handle splice operations, as it
triggers only one atom change instead of length amount of atoms, saving
memory and improving invalidation speed.
However, you can get the behavior you want where length is tracked
separately, as well as every individual item, you can use observable maps
that behaves exactly like that, with the downside that specific operations
like splice or unshift are much more expensive, if you'd mimic those
…On Sat, Dec 28, 2024, 21:13 Adam Frederick ***@***.***> wrote:
I'm not confused about how console.log behaves. This was run on the
command line, not browser dev tools. However, I'm getting the same results
now between the use of { deep: true }and without. Very odd, and sorry for
the confusion. I'll stick less into examples.
Here was my particular problem that initiated this
let observed;observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed.length)})
// triggersaction(() => observed.push('bill'))()// triggersaction(() => observed[1] = 'moo')()
output
Ran 2
Ran 3
Ran 3
Part of the object of mobx is to reduce re-rendering. Here we see that the
autorun that is observing .length runs when an element changes value,
even when the length doesn't change.
This is problematic because using .length is the solution given, if I
recall, to the common error of doing this:
let observed;observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed)})
// does not triggeraction(() => observed.push('bill'))()
This can be resolved by using computed, but I've never seen this in the
wild:
let observed;observed = observable(['moo', 'bob'])
let computed_length = computed(()=> {
console.log('computed')
return observed.length})
autorun(() => {
console.log("Ran", computed_length.get())})
// does triggeraction(() => observed.push('bill'))()// does not triggeraction(() => observed[1] = 'moo')()
output
computed
Ran 2
computed
Ran 3
computed
As I also pointed out in my example, this cross-property observation has
further problems than just .length :
let observed;observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed[0])})
// triggersaction(() => observed.push('bill'))()// triggersaction(() => observed[1] = 'moo')()
Output
Ran moo
Ran moo
Ran moo
Here, in attempting to observe just observed[0], the autorun runs on
observed[1] changes
—
Reply to this email directly, view it on GitHub
<#3988 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBCLXU4TV4IIMATRRIT2H4A7ZAVCNFSM6AAAAABUBXTNLKVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTCNRYGY3TSNI>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
I understand the performance issue with adding atoms for things like observed[0]. But, I've added a "changed" log to the Atom.reportChanged class let observed;
observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed.length)
})
action(() => observed[0] = 'mes')()
action(() => observed.push('bill'))() Before my hack, the output is Output
After hack, Output
This amounts to an additional reportChanged call per splice operation that results in a changed length. Is there a performance concern with this, or some other concern? |
Beta Was this translation helpful? Give feedback.
-
ExamplesInteresting. I don't recall the problem that prompted me, but I've made some example code. I see this issue can be a problem both with arrays of objects and arrays of primitives Array of Objects let data = new Data([{name:'bob'}, {name:'sue'}])
console.log(data)
const NameDisplayer = observer((props) => {
console.log('running NameDisplayer with '+props.person.name)
return (<span style={{paddingRight:'20px'}}>{props.person.name}</span>)
})
const NamesDisplayer = observer((props) => {
console.log('running NamesDisplayer')
return (props.people.map((person, i)=> <NameDisplayer key={i} person={person} />))
})
const LengthComputationDisplay = observer((props) => {
console.log('running LengthComputationDisplay')
return <div>Length Computation: {props.people.length * 10}</div>
})
const PeopleManager = observer((props) => {
console.log('re-run PeopleManager')
return ( <div>
<LengthComputationDisplay people={data.people} />
<NamesDisplayer people={data.people} />
</div> )
}) Doing
Here we see, when the object is replaced, the length computation re-runs. A lot of times, this length computation is not isolated into another component, but instead at the top level (PeopleManager), which would trigger the top level to re-run (PeopleManager). Array of Primitive Problem Abstracted
The CostThe cost is usually not If you look at something like this let observed;
observed = observable(['moo', 'bob'])
autorun(() => {
console.log("Ran", observed.length)
})
action(() => {
for(const k in observed){
observed[k] += '.'
}
})()
/*
without length Atom
changed
Ran 2
changed
changed
Ran 2
With length Atom:
changed
changed
Ran 2
changed
changed
*/ There is no reactivity cost to the length Atom apart from the initialization. So, the cost benefit analysis is more like:
Lets say most observed arrays are arrays of objects, and lets say the average array is 20 items long, then each array initialization costs at least 21 Atoms without a I don't think adding a |
Beta Was this translation helpful? Give feedback.
-
As you mentioned, the problem exists with ordering too. And, thinking about it, perhaps I encountered the problem with an array of inventory, when user swapped inventory spots (items didn't change, but the array got re-ordered). Now, let's suppose there were some logic on this that tracked whether an item was added or removed. if(old_length - x.length < 0){
// do length increase code
}else{
// do length decrease code
} Here, because it is expected that Like I said at the outset, my concern is with adoption of mobx. And that comes down to complexity and expectability. Here's the mobx dist code diff: |
Beta Was this translation helpful? Give feedback.
-
The example code looks like something that shouldn't pass any code review for not handling the equal case. That aside, the proper way to set up such a case where old and new value are compared is to use a reaction, and that wouldn't change either on a length change. The more fundamental problem however, is that you can always find a case where the thing you are object here holds. E.g. the length case was 'fixed', and you'd throw in a map or filter operation, you'd be exactly back at square one. E.g. something simple like So I think the mental model here is off. MobX is like a caching layer that tries to avoid downstream computations and effects. It is not a guarantee that in every case and circumstance it will be the theoretical minimum however, and systems should be robust against that (hence the importance of pure computations, and well orchestrated effects. This is very in a sense very similar to what React does with useMemo / useEffect, where it remains at React digression to invalidate those at moments it chooses). Doesn't mean that it is a bad idea btw to introduce that second atom, but just want to raise it might not solve the problem you think it is solving. |
Beta Was this translation helpful? Give feedback.
-
The short example code was just there to convey the concept - if there is the reasonable expectation that
Any way, I'm repeating my point now, and if it's not agreed, I will close this |
Beta Was this translation helpful? Give feedback.
-
Why is it expected? The initial run of the autorun does not map to any change. Why are you trying to infer what happenned from the fact that autorun is executed? It's unreasonable and fragile. Add another dependency and it's broken. Introduce action for resetting old_length also broken.
Your component logic surely doesn't depend on the exact number of re-renders or on what exactly caused the re-render. Same goes for |
Beta Was this translation helpful? Give feedback.
-
I will assume there is no agreement. I am closing this. |
Beta Was this translation helpful? Give feedback.
-
I like mobx. I built a complex form handler with it in 2021. Coming back to it, I've noticed unexpected behavior that I imagine throws many people off and is probably one of the reasons mobx is not more popular. I understand this is a byproduct of the behavior of proxies, but if you look at something like vue, they manage the
push
well (https://codepen.io/cfjedimaster/pen/GObpZM) . I imagine very few people who initially put a shallow observe on an array intended it to only observe a full swap (reference change).Shallow Array Unexpected Behavior
This last bit surprised me when I found a component observing x.length re running when x.length did not change.
Deep Array Unexpected Behavior
I wondered whether deep array observable would solve some of these problems. But, it results in more unexpected behavior.
Most annoying, observing
x.length
triggers on an element change regardless of whetherx.length
changes.Popularity and Changes
Reactivity with proxies is somewhat complex and that limits audience. Vue deals with this by hiding some things. For instance, in the https://codepen.io/cfjedimaster/pen/GObpZM example, the
methods.checkForm
is a sort of mobx action, where the errors observable changes only cause a single re-render. The vue developer doesn't need to know that.However, when you add on to the complexity unexpected behavior, like x.push handling, observing x.length, etc, you get even more of a fall off of audience.
Years ago when I started with mobx, I had a lot of frustration before I found
toJS
, which should be very apparent in the docs.Any way, I've not considered how to solve these things until there is an acknowledgement that they are problems and should be solved.
Beta Was this translation helpful? Give feedback.
All reactions