-
-
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
Bugfix #3392 #3404
Bugfix #3392 #3404
Conversation
🦋 Changeset detectedLatest commit: 6875f53 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 |
run |
@@ -908,3 +908,71 @@ test("Missing render should throw", () => { | |||
} | |||
expect(() => observer(Component)).toThrow() | |||
}) | |||
|
|||
test("this.context is not observable if ComponentName.contextType is not set", () => { | |||
const store = observable({ counter: 0 }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need any stores or observables here. All we have to test is:
Parent component has a state (eg number).
Update parent component state (eg increment the number).
Parent component passes this state (number) via context to child.
Child returns this.context
from @computed
.
Child returns @computed
in render.
Prior fix the child's output won't correlate with parent's state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better now?
|
||
console.log(compRef.current?.counterValue) | ||
|
||
expect(compRef.current?.counterValue).toBe(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried to run the test without the fix? I think this would pass even before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This passes in both cases. But this why I used the store before. Like here
store = observable({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A test that passes before and aft doesn't sound like a good test :) I can't see the original test clearly anymore, would you mind adding that as well for now at least? I do think atm this PR is correct, but that it isn't the "correct" solution to the original problem, from scanning the original issues 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can see the original test in this commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify; in the original problem your 'store' isn't a store at at all, it is an unstable object reference for something that should be stable in the mobx idioms, and the correcter way to solve it would be simple: <FormContext.Provider value={this}>
and skip that whole computed thing. Computed values shouldn't normally be new objects that are observable / mutable; either they are a ref to an already existing object, or they are immutable value objects, just representing the derived value at a certain moment in time.
In your current setup you create many objects that might all live at the same moment in time, representing many different selections.
Edit: fixed snippet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just use the computed value inside the render and it will fail. The computed acts as plain getter if it's not observed, therefore the problem doesn't occur.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, for the first test, yeah that didn't seem to make sense indeed, that should pass even if context would be an observable.
} | ||
|
||
render() { | ||
return <div /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what you want to test here as well is <div>{this.counterValue}</div>
, is getting rerendered (once) to make sure that the @observer
as well picks up the change in the computed value, as createObservableProp sets some internal flags, for which I'm not 100% sure that is correct in the case of context (I suspect it is correct)
New test is up. It checks
The |
And funny thing I discovered is https://codesandbox.io/s/heuristic-mayer-th9058?file=/src/App.tsx |
|
||
let renderCounter = 0 | ||
|
||
@observer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks this test looks a lot better! Still want to point out that this component doesn't have to be an observer at all if you'd put an observable object as context value, saving re-rendering the root of the tree and any unmemo-ed child thereunder, whenever you increase the counter. That is the way it is typically done, and I suspect also the reason why no one reported this earlier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What difference does it make to the render passes whether the context value is in an observable or in a computed?
The component must react either way to pass the new value to the context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In your setup, it is Parent
that reacts to a change in counter
, by creating a new object (at line 929) as part of the render process to compute this.contextValue
(you basically make an unnecessary state copy here). If you'd pass a stable observable object (or even this
) as Provider value, the counter itself isn't read at all by the Parent, but instead by the Child, so that the effect of changing counter
would be much more local effect. So instead of re-rendering in principle the whole subtree of Parent, you'd only be re-rendering individual Child components.
The component must react either way to pass the new value to the context.
So, it is not necessary at all to pass a new value as context :) You don't have to allocate a new store if you can just keep using the same store and update it's internal state instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So basically cutting the Reacts render logic completely out and using parent only as Context Provider? This is kind of strict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In real life I often have something like this:
@computed
get contextValue(): DropdownContextType {
return {
toggleDropdown: this.toggleDropdown,
closeDropdown: this.closeDropdown,
open: this.open,
type: this.props.type || this.computedType,
};
}
and this.open
is also accessed in render to determine if dropdown should be rendered open or not. So it doesn't matter here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the benefit of constructing that object over and over again over just passing this
(you strongly typed out the other members anyway). If you really want to strict down the object passed, you could also allocate it once and just make it a delegator object, so that you can keep using the same contextValue
even when you're state changes over time:
contextValue = {
toggleDropdown: this.toggleDropdown,
closeDropdown: this.closeDropdown,
get open() { return this.open },
get type() { return this.props.type || this.computedType }
}
So basically cutting the Reacts render logic completely out and using parent only as Context Provider?
If there is no need for it yes, if there is need for it, fine as well. In you're cases shown so far there is no need (as in, there is no visual changes to your UI 'above' the Child components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this way you get into nasty this
bugs.
https://codesandbox.io/s/admiring-stallman-hcnrpv?file=/src/Form.tsx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your help and support! And for Mobx obviously! Luckily it came out early enough so that I didn't dipped to much in Redux.
@urugator can this PR merged? |
Thank you |
Bugfix #3392
Code change checklist
/docs
. For new functionality, at leastAPI.md
should be updatedyarn mobx test:performance
)