Repository navigation
fix: read the parent @computed through super before the override is read - #4749
nk-batman21 wants to merge 2 commits into
Conversation
With 2022.3 decorators, the parent getter wrapper only checked values_, which is still empty while the subclass @computed override is lazy. It then fell through to getObservablePropValue_, which materialized and evaluated the subclass factory, so super.x returned the override value. Materialize the entry before the derivation check, so the parent read goes to its own computed. Un-skips the mobxjs#4660 test added in mobxjs#4661.
🦋 Changeset detectedLatest commit: 50c95bc The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
MobX documentation has an amazing advice regarding inheritance:
https://mobx.js.org/subclassing.html I'll leave a couple of examples that will produce a wrong result if the PR is merged, based on Claude's findings: import { autorun, computed, observable, runInAction } from "../../src/mobx"
test("parent constructor reaction on an overridden @computed", () => {
const log: number[] = []
class Parent {
@observable accessor price = 100
constructor() {
autorun(() => log.push(this.total))
}
@computed get total() {
return this.price
}
}
class Child extends Parent {
@observable accessor discount = 0.5
@computed override get total() {
return super.total * this.discount
}
}
const c = new Child()
runInAction(() => (c.price = 2))
runInAction(() => (c.discount = 1))
expect(log).toEqual([100, 1, 2])
})
test("parent constructor reads an overridden @computed", () => {
class Parent {
@observable accessor price = 100
initialTotal: number
constructor() {
this.initialTotal = this.total
}
@computed get total() {
return this.price
}
}
class Child extends Parent {
@observable accessor discount = 0.5
@computed override get total() {
return super.total * this.discount
}
}
expect(new Child().initialTotal).toBe(100)
}) |
|
@kubk thanks, both examples broke with the first version. 50c95bc narrows the change: the getter only materializes the override first when the running getter is not the one |
With 2022.3 decorators,
super.xreturns the subclass override instead of the parent value if it runs before the subclass's own@computedoverride ofxwas read on that instance. This is the case of the skipped test that #4661 added for #4660.In an
autorunorobserverthat readssavingson a fresh instance, the wrong 0 is what gets cached, and it stays untilpricechanges.Since the computeds became lazy, the parent getter finds no entry in
values_yet and falls through togetObservablePropValue_, which materializes the last registered factory (the subclass one) and evaluates the override. When there is no entry yet and the running getter is not the onethis[key]resolves to, which is the case for asuperread, the getter now materializes the entry first, so the existingderivation !== getcheck sends the parent read to its own computed. A plainthis.xread, including one in a parent constructor before the override is registered, takes the same path as before, and so does every read that already has an entry.I un-skipped the test and shortened the comment above it, since it described the bug. Happy to keep the original comment if you prefer. Two more tests pin the parent constructor cases from the review: a read in the constructor returns the parent value, and an
autorunstarted there follows the override once it is registered.Code change checklist
/docs. For new functionality, at leastAPI.mdshould be updated (not needed for this fix)npm -w mobx run test:performance)Ran: the mobx suite (798 passed, 9 skipped; before: 795 and 10),
tsc --noEmit, eslint and prettier on the changed files,npm run check-sizeand the lazy@computedbenchmark. The mobx-react, mobx-react-lite, eslint-plugin-mobx and mobx-undecorate tests ran on the first commit.Checked:
mobx.cjs.production.min.jsgrows by 124 bytes (61 gzipped); the lazy@computedbenchmark, run alternately againstmain, shows no difference beyond run-to-run noise.