-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
unintuitive behavior when an observable is created and then observed within the same computed #2096
Comments
I wonder, where did you actually get the idea that creating observable in computed is a good thing? There is no such recommendation in the linked issue. From my limited understanding, the computed is deemed to rerun and your observable is recreated from scratch and throwing away the previous state. Also, note that issue is 3 years old and talking about MobX3 which is no longer maintained. I don't know what exactly changed there, but I wouldn't recommend getting inspiration from such an old issue. |
The linked issue was specifically about allowing to create observables within a computed and not produce a strict violation. As a result this pr #812 was produced which explicitly enabled this behavior (In Mobx 3 yes but it's true for 4/5 as well).
Why would creating observable values in computed be not a good thing in your opinion? Observable values are values after all and that's what a computed is supposed to produce. We currently use this pattern in production with the only caveat being this particular issue. |
Not sure just doesn't feel right 😆But hey, I am using like half of MobX features, so don't listen to me. I suppose it cannot hurt if you proceed with PR. Be sure to add tests. Start at the |
At first I wanted to offer configure({ enforceActions: "observed" });
class Foo {
@computed
get store() {
return new Store({ name: 'foo' });
}
}
class Store {
@observable obj;
@observable name;
constructor(obj) {
this.obj = obj;
const name = this.obj.name; // subscribed to obj.name
// statisfy enforceActions
runInAction(() => {
this.name = name;
});
}
}
const f = new Foo();
autorun(() => f.store);
const g = f.store;
console.log(g === f.store) // true
// statisfy enforceActions
runInAction(() => {
g.obj.name = 'new name';
})
console.log(g === f.store) // false It will also yield I think there are some problems with the suggested solution:
autorun(() => {
computed(() => o.x + o.y).get();
}) I know this is unaffected by the PR, but raises some questions about how it plays together.
const cmp = computed(() => {
const o = computed(() => {
// create observable inside nested derivation
const o = observable({ x: 1 });
o.x; // subscribe (fine)
return o;
}).get();
o.x; // subscribe (derivation id mismatch)
return o;
})
// then somewhere
const o = cmp.get();
console.log(o === cmp.get()) // true
o.x = 2;
console.log(o === cmp.get()) // false So perhaps we would need a
There are 2 associated problems:
|
[Assuming you meant const cmp = computed(() => {
const o = computed(() => {
const o = observable({ x: 1 });
o.x;
o.x = 5; // no problem
return o;
}).get();
o.x;
o.x = 10; // will throw
return o;
}) Essentially what I would like is to have both the read and write be silent for observables that are created within a computed for that computed only (currently only write is). Not sure what the best way to achieve that is, using something like
Yeah but these issues are not unique to observables but rather to non primitive values, so For example we might generate a computed store that's based on some observable selection, that store itself will have a bunch of observable state related to that selection and when that selection changes we want to generate a new store and reset all state associated with the previous selection. More concretely, say we have a list of items on the right and a detail view on the left, when a user selects an item we want to generate a details store which also has some state on it. When they select another item, we want to dispose of the previous details store and create a new one based on the new selection. We achieve this using a computed method which generates a new store. class MyRootStore {
@observable selection = null;
@computed get detailStore() {
if (this.selection) {
return new DetailStore(this.selection);
}
return null;
}
} Hope that makes sense. |
I am assuming there is some observer, same situation as above.
Interesting. I would expect it to go somehow like: I briefly checked that Why we want it to yield
It does, I used to do it quite often, but didn't work well for me in the end. |
My mistake, I was using
You're right, we want |
Hm, but disabling subscription for the whole reaction is a no-no: @computed
get baz() {
const store = this.foo.store; // creates new store (therefore all it's props are excluded from subscription inside current reaction)
// still inside same reaction
return store.name; // not subscribed!
} It makes sense only if the whole graph is created inside reaction: @computed
get baz() {
const foo = new Foo();
const store = foo.store; // creates new store (therefore all it's props are excluded from subscription inside current reaction)
// still inside same reaction
return store.name; // not subscribed - but in this case it's correct, because store.name will always yield the same value
} |
Yup, as long as the computed is created and evaluated within another the proposed behavior applies. When I get some time I'll update the PR with what we discussed. |
Just for completeness - it applies to render() {
const store = this.props.foo.store;
return store.name; // should susbcribe
} vs whole graph created inside derivation render() {
const foo = new Foo();
const store = foo.store;
return store.name; // shouldn't subscribe
} |
Hm, what about let o;
let x = observable.box(1);
autorun(() => {
o = o || observable.box(1);
x.get();
o.get();
});
o.set(2) // don't re-run
x.set(2) // re-run
o.set(3) // re-run ? Gets a bit trickier with reactions as the above pattern is valid (?) while it's an anti-pattern with computed. |
On the second run ( |
Yeah not to mention that if we apply this to reactions, things like Maybe the PR is ok as-is , it will re-evaluate needlessly on nested computes but that's not a change in behavior and is consistent with how write works while fixing the common case of creating observables in computed functions. |
Isn't it the same thing? If I call |
Yeah, I somehow missed that since no computed-related tests broke with my PR. Alright it seems like it's a no-go then and this issue and PR can be closed as we have no way to track whether or not an observable gets leaked outside of the computed/reaction. Thanks for your input on this. I can modify that PR / create a new one to add a unit test for the use |
To summarize: If an observable is created within a derivation we do not want to subscribe to it if the result of re-running the derivation will just end up re-creating the observable. As this will not produce a new value when the derivation is a computed and might lead to surprising behavior as documented in the initial issue comment. Yet a valid use case might be to create an observable lazily within a derivation and cache it for remaining runs. In this case we do want the derivation to re-run when the observable changes. Otherwise the derivation will be stale. Problem is that there's no way for mobx tell within a derivation whether or not the created observable will be re-recreated each time or just lazily initialized. It would have to be up to the consumer to be aware of this issue and use an api to flag it. Currently all observables created within a derivation behave as though they are lazily initialized and the consumer would have to use |
I have a:
Within a computed function you're allowed to create an observable and then modify it. This makes perfect sense and was resolved in issue #563. Yet if you then proceed to read from the observable you just created within the computed function, it will be observed. Which can lead to some surprising and unintuitive behavior.
Consider the following example:
This will log
but within the constructor if you change
this.name = obj.name;
tothis.name = this.obj.name
you would expect the same behavior but instead you will now get:since
this.obj
is now observed by the computed even though thethis
was created within that same computed.The proposal here would be to make created observables completely silent for both read and write operations to the computed function that created them.
This will avoid the behaviour outlined above but also avoid a needless re-computation since changing an observable produced by a computed will not change the value that the computed produces.
Example:
It is possible to produce a different value but that would involve modifying outside state within the computed making it an anti-pattern.
The text was updated successfully, but these errors were encountered: