mobx
mobx copied to clipboard
observe inside reaction effect cause unobservation after effect done
I'm not sure how to describe this problem. In my project, I'm trying to create a deep nested hierarchical structure of some classes that is constantly changing, so I need to optimize everything as much as possible. The tree can be very large. I need to monitor the entire tree only when I have some state enabled and destroy it when it is disabled. So, I have some code inside reaction that reacts to the 'active' flag and creates or destroys the entire tree. But I noticed that all nested instances become unobservable and are recreated after the reaction (effect) is completed. If I run my code outside of reaction (for example, if I run it in setTimeout), it runs as I expect.
Maybe I'm doing the wrong thing and should rewrite my code, but now it looks like a bug in Mobx. I tried to solve my problem using createTransformer or deepObservable, but I need more control and they didn't suit me, so I just use some approaches from their source code.
I use mobx 5 in my project, but I did a simplified demo at the link below, and it works the same way with mobx 6.9.0
Intended outcome:
Actual outcome:
How to reproduce the issue: CodeSandbox
Versions
5.x.x - 6.9.0
Despite the name, observe
and onBecome(Un)Observed
are completely unrelated.
onBecome(Un)Observed
is only relevant for observables accessed in autorun
/reaction
/observer
.
An observable becomes observed when it's first accessed by some autorun
/reaction
/observer
.
An observable becomes unobserved when no autorun
/reaction
/observer
depends on it - either all were disposed or the observable was accessed conditionally and the condition changed.
@urgator you are completely wrong, because observe just calls the observe_ method for any Atom/Computed/Observable, and they in turn call autorun under the hood.
Look at the implementation of computed, for example: https://github.com/mobxjs/mobx/blob/27efa3cc637e3195589874990c23d4de82c12072/packages/mobx/src/core/computedvalue.ts#L279C9-L279C9
For this reason, these code examples will work completely the same:
with observe:
state = mobx.observable({ a: 1, b: 2, get c() { console.log('get c'); return this.a * this.b } });
mobx.onBecomeObserved(mobx.getAtom(state, 'c'), () => console.log('c observed'));
mobx.onBecomeUnobserved(mobx.getAtom(state, 'c'), () => console.log('c unobserved'));
disposer = mobx.observe(state, 'c', () => {}); disposer();
with autorun:
state = mobx.observable({ a: 1, b: 2, get c() { console.log('get c'); return this.a * this.b } });
mobx.onBecomeObserved(mobx.getAtom(state, 'c'), () => console.log('c observed'));
mobx.onBecomeUnobserved(mobx.getAtom(state, 'c'), () => console.log('c unobserved'));
disposer = mobx.autorun(() => state.c); disposer();
Anyway, my example just shows a very strange mobx behavior in a very unusual situation, however it may be related to other issues. I've been using mobx for many years, and this is the first time its behavior seems wrong to me, and I'm sure it's not my fault.
However, I rewrote my code in a different way and solved my problems. But I want mobx developers to see the problem and explain if this is the expected behavior and why.
Hm, I was under the impression it just registers the listener. I think the computed
is just an exception here, since it has no setter, so there must be a reaction that actually calls the listener.
Conceptually however, I think, it's a bit awkward, becase normally observe
triggers immediately on mutation - it doesn't wait for transaction (like derivations do), but since computeds are lazy and listener is triggered by autorun, the semantics are different.
For the same reason, I think onB(U)O is only called for computed
, but not for non-computed observables.
Yes, you're right, it looks like computed
is an exception. My bad.
In any case, in my issue question I get not the expected onBecomeUnobservable
, so our discussion is hardly related to the problem.
In fact, I use observe in the example just to make my computed "keepAlive", it can be replaced with reaction or autorun
Btw, not sure if it's related and I am not sure if it's still the case, but IIRC onB(U)O is (used to be) called at the start/end of action for computed, even without an actual derivation, because mobx setups temporal "observer" for the duration of the action to keep the computed hot - so it's recomputed only once in case you access it multiple times in that single action.
Another thing, that I am not sure is relevant, but worth noting, is that if you attempt to setup derivation in action, the derivation won't run until the action is finished, otherwise it could read inconsistent state:
runInAction(() => {
this.stateX = 1;
autorun(() => {
console.log("autorun", this.stateX, this.stateY);
computed.get(); // subscribe to computed
})
this.stateY = 2;
console.log("action end") // prints before "autorun"
})
But you better double check it.
Exactly! You damn right and I think it's fully relevant to issue. I totally understand what any derivations in actions won't run until the action will be finished and it's fine. But why mobx automatically runs any reaction effects as actions?
// Action
state = mobx.observable({ x: 1, y: 1 });
mobx.runInAction(() => {
console.log("action start");
state.x += 1;
mobx.autorun(() => {
console.log("autorun", state.x, state.y);
});
state.y += 1;
console.log("action end"); // prints before "autorun" and its fine
});
// Effect
state2 = mobx.observable({ x: 1, y: 1, active: false });
mobx.reaction(() => state2.active, () => {
console.log("action start");
state2.x += 1;
mobx.autorun(() => {
console.log("autorun", state2.x, state2.y);
});
state2.y += 1;
console.log("action end"); // prints before "autorun" and I'm not sure that its fine
});
state2.active = true;
All my problems caused by it, because I do something like component mount (its not a React) inside reaction effect and as it runs as action - any observations with mobx in that component runs not immediately. And also I can get observe/unobserve/observe flow in some cases (but I can't reproduce it right now).
So now I need async my effects every time I use it. But its terrible, I don't want to wrap all my code to setTimeout :D It should work synchronous. Maybe there is exist any way to move over this behaviour?
I made another super simple example that shows exactly what is going on in my code: https://codesandbox.io/s/mobx-computed-in-effect-forked-psc5wl?file=/src/index.ts
Sorry, only scanned the last question quickly, but might be missing something, but it seems the same question / answer as posted here :) Let me know if that isn't useful.