Why don't getters use cached value when accessed directly? #2161
-
I have a vague understanding that this has something to do with how MobX reactivity works, but I would like a more thorough understanding and learn some potential options for dealing with it. import { when } from "mobx";
import { types } from "mobx-state-tree";
import { expect, test } from "vitest";
let calls = 0;
const Model = types.model("Model", {
a: 1,
b: 2,
}).views(self => ({
get sum(): number {
calls += 1;
return self.a + self.b;
},
}));
// In this first test, we just access the getter a few times, and
// it runs the getter function each time
test("Only one call?", () => {
calls = 0;
const m = Model.create();
expect(calls).toMatchInlineSnapshot(`0`);
expect(m.sum).toMatchInlineSnapshot(`3`);
expect(calls).toMatchInlineSnapshot(`1`);
expect(m.sum).toMatchInlineSnapshot(`3`);
expect(calls).toMatchInlineSnapshot(`2`);
});
// But in this test, we wrap everything in a `when` and
// now the getter only runs once
// See: https://github.com/mobxjs/mobx-state-tree/discussions/2102
test("When?", async () => {
calls = 0;
const m = Model.create();
expect(calls).toMatchInlineSnapshot(`0`);
when(() => true, () => {
expect(calls).toMatchInlineSnapshot(`0`);
expect(m.sum).toMatchInlineSnapshot(`3`);
expect(calls).toMatchInlineSnapshot(`1`);
expect(m.sum).toMatchInlineSnapshot(`3`);
expect(calls).toMatchInlineSnapshot(`1`);
})();
expect(calls).toMatchInlineSnapshot(`1`);
}); What's going on here, and what are some ways I can ensure only-once getter execution when I'm basically treating the MST instance as a plain object? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 8 replies
-
This seems to be another option. Is it a good one? Is this a bad idea for some reason? class Wrapper {
private _sum: number | undefined = undefined;
constructor(model: Instance<typeof Model>) {
autorun(() => {
this._sum = model.sum;
});
}
get sum(): number | undefined {
return this._sum;
}
}
test("Wrapper", async () => {
calls = 0;
const m = new Wrapper(Model.create());
expect(m.sum).toMatchInlineSnapshot(`3`);
expect(calls).toMatchInlineSnapshot(`1`);
expect(m.sum).toMatchInlineSnapshot(`3`);
expect(calls).toMatchInlineSnapshot(`1`);
expect(m.sum).toMatchInlineSnapshot(`3`);
expect(calls).toMatchInlineSnapshot(`1`);
}); |
Beta Was this translation helpful? Give feedback.
-
Hey @dbstraight - thanks for your question, and thanks for opening up a new thread. I had missed #2102 (comment) originally, and saw your comment there. I don't know the exact code where this is happening, but I suspect it's because the cache has to happen inside whatever is observing the MobX instance. I mostly write React Native with MobX-State-Tree. So when I access computed views like this, it's always cached. I would guess that caching behavior is actually a byproduct of a MobX observable + an observer. So for MST, something like MST store + The nice thing is, once it's observed at all, it caches for everyone. Check this out: import "./styles.css";
import { observer } from "mobx-react-lite";
import { types } from "mobx-state-tree";
let calls = 0;
const Model = types
.model("Model", {
a: 1,
b: 2,
})
.views((self) => ({
get sum(): number {
calls += 1;
return self.a + self.b;
},
}));
const m1 = Model.create({ a: 3, b: 4 });
const m2 = Model.create({ a: 5, b: 6 });
function App() {
return (
<div className="App">
<DoesNotCache />
<Caches />
</div>
);
}
const DoesNotCache = observer(() => {
return (
<button
onClick={() => {
console.log(m1.sum);
console.log(calls);
}}
>
Not Cached
</button>
);
});
const Caches = observer(() => {
const { sum } = m2;
return (
<button
onClick={() => {
console.log(sum);
console.log(calls);
}}
>
Cached
</button>
);
});
export default observer(App); Notice I have two model instances. Because if you try this demo with just one instance and include the cached component, the computed value gets cached. The observer HOC is not smart enough (or unwilling, I imagine) to actually tell Again, I don't know the actual code where this is specified, but the solution here is: observe your observable in at least one place and you'll get the caching behavior. If your observable is not observed (or if it doesn't know that it has been), I don't think the caching mechanisms will kick in. |
Beta Was this translation helpful? Give feedback.
So here is where we set up the getters. It calls
makeObservable
from MobX with these options:Which is also calling the MobX
computed
option here: https://mobx.js.org/computeds.htmlI don't know enough about the MobX codebase to actually say how this interaction happens. That's on them. But since MST is built on top of MobX, their runtime behavior becomes ours, haha.