-
-
Notifications
You must be signed in to change notification settings - Fork 348
Component-class with @observer doesn't dispose reaction if render() is hot-swapped #797
Comments
Huh, hot-swapping render? Sounds like a nasty hack :) Never understood this weird desire to have hooks in classes. It's messed up idea imo to have two ways of handling side effects and state. In my personal opinion, I don't think this should be supported in the library. If you are feeling for it, make a PR with proper tests and let's see what happens next. |
I was afraid of mentioning the This issue was opened because of the potential memory leaks (and slowdown from persistent, unneeded reactions) caused by "late hot-swapping" from any source -- not just Want an example of a library other than The conflict only happens because both libraries are using hot-swapping, and mobx-react uses fragile hot-swapping -- that is, it assumes it's the only one hot-swapping the function. (That is, it attaches data to its override and assumes it will remain hoisted at That's an unwarranted assumption imo, and its conflict with As mentioned above, there are two potential solutions to it.
Either sounds better to me than the current state, where |
Yea, as I said, feel free to make a PR with a solution and if it turns out not invasive too much without breaking too many things, we might consider merging it. |
Okay, sounds good. I suspect it is as simple as changing the two lines that access |
Just do the swapping / if check inside the render? Or copy all own members
of the original function onto your new render function?
…On Tue, Nov 12, 2019 at 8:05 AM Stephen Wicklund ***@***.***> wrote:
For a React component-class with the @observer decorator, instances of
the class do not dispose their MobX reactions, if their render() method
is overridden.
For example:
class MyButton extends Component {
render() {
return <button>MyButton</button>;
}
render_withLogging() {
console.log("Start");
this.render();
console.log("End");
}
componentDidMount() {
if (window.enableLogging) {
this.render = this.render_withLogging;
}
}
}
Since the component above hot-swaps the render function, the
this.render[mobxAdminProperty] attachment becomes inaccessible to the
following code in mobx-react's patched version of componentWillUnmount():
https://github.com/mobxjs/mobx-react/blob/a4099e86f07b3c0778da92663a82746c077c1756/src/observerClass.js#L36-L40
Become the reaction (stored under the mobxAdminProperty key) is
inaccessible, the call to [reaction].dispose() is short-circuited,
meaning it never gets cleaned up.
For reference, here is where that special property is first attached: (it
only runs the first time the component is rendered, so its hot-swapping of
the render function is not guaranteed to be final)
https://github.com/mobxjs/mobx-react/blob/a4099e86f07b3c0778da92663a82746c077c1756/src/observerClass.js#L88-L89
Real-world library conflicts
The reason I discovered the issue is because I've been using this library
to enable hooks in react class-components:
https://github.com/salvoravida/react-class-hooks
It conflicts because, as alluded to above, the library hot-swaps the
render function of component instances just like mobx-react does, except it
does it even later than mobx-react. (when a hook is first used, rather than
when the render function merely begins)
Potential solutions
1. Attach the reaction (ie. value under mobxAdminProperty) to the
class instance rather than the render function.
2. Patch the this.render to be a getter+setter rather than a property,
and code the setter such that, if the value gets replaced, the special
mobxAdminProperty property (ie. the reaction) gets transferred from
the old render-function to the new one.
I favor the first solution since it doesn't add another level of
indirection.
Thoughts?
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#797?email_source=notifications&email_token=AAN4NBC2NU7UAIAWMZZ75FDQTJPUPA5CNFSM4JL7WR72YY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4HYTZBFA>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBHK7J2AKUGBTUXXTMDQTJPUPANCNFSM4JL7WR7Q>
.
|
I'm not sure what you mean.
If the only source of At the very least, I think |
For reference, I've created this test to make clear what's going wrong: (the test is currently failing)
|
That would probably look like:
(might need a different way of enumeration depending on the enumerability of the original function) By the way, I doubt that doing the above actually will enable logging reliable, as mobx-react IIRC captures the original (That all being said, I personally don't get why one would want hooks in a component class (I mean, wouldn't just refactoring the then not be a lot more intuitive). But I even less get that why do that for an |
Just as a head up: We probably won't accept a PR unless it is a really trivial change on our side, for the simple reason that the above library isn't significant enough yet to warrant the significant additional complexity on maintenance of this one while there are clear work arounds (such as splitting the component in 2, using Btw, not patching render methods is a documented limitation of mobx-react (the simple case of |
Yeah, I think that would work fine for a project developer's own swapping of this.render, but it's not really a solution for cases where third-party libraries perform the swapping.
I agree that the usefulness of hooks in component classes goes down a lot when integrating MobX. My project used to use Redux, so I wasn't able to use external state as liberally (since connect functions have a higher overhead than MobX dependency trees), leading to me using local class variables for some complex behavior. One place where hooks are still useful in component classes is for the "useCallback" hook -- enabling you to only have a callback updated when a variable it uses is changed. This improves performance by letting the children components use React.PureComponent to not re-render if no props have changed. Another place it's useful is for using third-party libraries that provide functionality nicely wrapped in "useEffect" (thus requiring hooks). For example: https://github.com/rehooks/component-size But anyway, that's just the library that made me detect this issue; even if
Well, I created this PR for it just now: #798 The modification used to make the fix seems pretty straight-forward/trivial to me; it's just attaching the reaction to the class instance instead of the render function. |
Note to self: check compatibility with devtool packages, to probably pick
the meta data from render instead, making this a breaking change.
…On Tue, Nov 12, 2019 at 10:58 AM Stephen Wicklund ***@***.***> wrote:
That would probably look like: [...]
Yeah, I think that would work fine for a project developer's own swapping
of this.render, but it's not really a solution for cases where third-party
libraries perform the swapping.
(That all being said, I personally don't get why one would want hooks in a
component class (I mean, wouldn't just refactoring the then not be a lot
more intuitive). But I even less get that why do that for an observer
component, as you can leverage all mobx primitives for managing side
effects already)
I agree that the usefulness of hooks in component classes goes down a lot
when integrating MobX. My project used to use Redux, so I wasn't able to
use external state as liberally (since connect functions have a higher
overhead than MobX dependency trees), leading to me using local class
variables for some complex behavior.
One place where hooks are still useful in component classes is for the
"useCallback" hook -- enabling you to only have a callback updated when a
variable it uses is changed. This improves performance by having letting
the children components use React.PureComponent to not re-render if no
props have changed.
But anyway, that's just the library that made me detect this issue; even
if react-universal-hooks did not exist, I'd still think it's worth fixing
this issue for other libraries that might try to use swapping of
this.render.
We probably won't accept a PR unless it is a really trivial change on our
side
Well, I created this PR for it just now: #798
<#798>
The modification used to make the fix seems pretty
straight-forward/trivial to me; it's just attaching the reaction to the
class instance instead of the render function.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#797?email_source=notifications&email_token=AAN4NBFUJ5AQ6Z2FEFPIR23QTKD4BA5CNFSM4JL7WR72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDZ3VTY#issuecomment-552843983>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBGF5UUNX5W5ZRMMJBTQTKD4BANCNFSM4JL7WR7Q>
.
|
Did you check if the logging now actually works? As said I suspect the
swapping is a no-op when it comes to reactively triggered renders
On Tue, Nov 12, 2019 at 11:05 AM Michel Weststrate <[email protected]>
wrote:
… Note to self: check compatibility with devtool packages, to probably pick
the meta data from render instead, making this a breaking change.
On Tue, Nov 12, 2019 at 10:58 AM Stephen Wicklund <
***@***.***> wrote:
> That would probably look like: [...]
>
> Yeah, I think that would work fine for a project developer's own swapping
> of this.render, but it's not really a solution for cases where third-party
> libraries perform the swapping.
>
> (That all being said, I personally don't get why one would want hooks in
> a component class (I mean, wouldn't just refactoring the then not be a lot
> more intuitive). But I even less get that why do that for an observer
> component, as you can leverage all mobx primitives for managing side
> effects already)
>
> I agree that the usefulness of hooks in component classes goes down a lot
> when integrating MobX. My project used to use Redux, so I wasn't able to
> use external state as liberally (since connect functions have a higher
> overhead than MobX dependency trees), leading to me using local class
> variables for some complex behavior.
>
> One place where hooks are still useful in component classes is for the
> "useCallback" hook -- enabling you to only have a callback updated when a
> variable it uses is changed. This improves performance by having letting
> the children components use React.PureComponent to not re-render if no
> props have changed.
>
> But anyway, that's just the library that made me detect this issue; even
> if react-universal-hooks did not exist, I'd still think it's worth
> fixing this issue for other libraries that might try to use swapping of
> this.render.
>
> We probably won't accept a PR unless it is a really trivial change on our
> side
>
> Well, I created this PR for it just now: #798
> <#798>
>
> The modification used to make the fix seems pretty
> straight-forward/trivial to me; it's just attaching the reaction to the
> class instance instead of the render function.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <#797?email_source=notifications&email_token=AAN4NBFUJ5AQ6Z2FEFPIR23QTKD4BA5CNFSM4JL7WR72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDZ3VTY#issuecomment-552843983>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAN4NBGF5UUNX5W5ZRMMJBTQTKD4BANCNFSM4JL7WR7Q>
> .
>
|
Good point. Just tested, and component renders triggered by MobX do not call into a newly-swapped-in this.render function. For example this fails:
I suppose that a proper fix would therefore require a more substantial reworking of mobx-react's render-func wrapping. (while my PR fixes the particular bug in MobX, it doesn't solve the wider problem of MobX not having a reference to the new I don't have time/motivation for that "proper fix" right now, so I'll close the pull-request since it's incomplete. Might be worth keeping this issue open for now though, since Adding something like this to mobx-react's patched
|
Feel free to make a PR with a warning, otherwise, I don't think we are too keen supporting something like that. Perhaps it will help other people to realize that trying to mix hooks and classes is a bad idea :) |
The warning sounds good. I think we have a similar check already in place
to check for `render = () => { }` at mounting time. But that obviously
misses this case :) (btw, note that cWU can be swapped out at exact the
same way as render and then you'd end up with the very same problem (both
in mobx-react and your lib). Which is kinda the problem hooks fundamentally
solve)
…On Tue, Nov 12, 2019 at 11:27 AM Daniel K. ***@***.***> wrote:
Feel free to make a PR with a warning, otherwise, I don't think we are too
keen supporting something like that. Perhaps it will help other people to
realize that trying to mix hooks and classes is a bad idea :)
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#797?email_source=notifications&email_token=AAN4NBAMDM5HAFLHCY6IGPLQTKHLNA5CNFSM4JL7WR72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDZ6GYQ#issuecomment-552854370>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBDXXFLCE7JYAKWAB4LQTKHLNANCNFSM4JL7WR7Q>
.
|
Okay, a warning would be good enough for me. (assuming the "proper fix" would require a substantial mobx-react rework) I mainly just didn't want others to hit the "hidden bug" of lots of MobX reactions unknowingly leaking / persisting past component lifecycle. (For my particular usage of |
…nction after a MobX reaction has already attached. Helps prevent memory leaks as in: [mobxjs#797](mobxjs#797) * Added test for the above. * Added changelog entry.
Created a pull-request (#799) that adds a warning for the issue (replacing If one needs to do so anyway, here is a complete example of how you can, while preventing the leaking/persistence of the MobX reaction for the component:
However, some drawbacks:
|
A better way than the above (because of issue 2 mainly), is actually just to make sure the wrapper you want to place around Actually, that won't work currently because the class-decorator stores a reference to the class prototype's
To this:
EDIT: Nevermind, the above won't work, because it will cause an infinite loop -- when the 3rd-party lib's wrapper-func is created, it will reference the mobx original on-class wrapper-func, meaning that if mobx's final on-instance Technically one could probably make mobx-react compatible with this case, by having the on-class wrapper-func recognize it and just call the original on-class render-func instead of attaching a new on-instance wrapper-func. But this is confusing enough that I fully understand the Like always, if one absolutely needs a given change in library behavior, you can always either fork the library (hard / high maintenance), or use this (easy -- though requires care to not make mismatches between the string-replace code and the lib version marked in package.json). |
Since I don't see a clear path forward to making mobx-react compatible with hot-swapping of If someone thinks of a good solution (that isn't confusing like the last option listed above), feel free to bring it up. As of now, I'll find some other way to integrate the libraries that use |
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or questions. |
[mobx-react version: 6.1.4]
For a React component-class with the
@observer
decorator, instances of the class do not dispose their MobX reactions, if theirrender()
method is overridden.For example:
Since the component above hot-swaps the render function, the
this.render[mobxAdminProperty]
attachment becomes inaccessible to the following code in mobx-react's patched version ofcomponentWillUnmount()
:mobx-react/src/observerClass.js
Lines 36 to 40 in a4099e8
Become the reaction (stored under the
mobxAdminProperty
key) is inaccessible, the call to[reaction].dispose()
is short-circuited, meaning it never gets cleaned up.For reference, here is where that special property is first attached: (it only runs the first time the component is rendered, so its hot-swapping of the render function is not guaranteed to be final)
mobx-react/src/observerClass.js
Lines 88 to 89 in a4099e8
Real-world library conflicts
The reason I discovered the issue is because I've been using this library to enable hooks in react class-components: https://github.com/salvoravida/react-class-hooks
It conflicts because, as alluded to above, the library hot-swaps the render function of component instances just like mobx-react does, except it does it even later than mobx-react. (when a hook is first used, rather than when the render function merely begins)
Potential solutions
mobxAdminProperty
) to the class instance rather than the render function.this.render
to be a getter+setter rather than a property, and code the setter such that, if the value gets replaced, the specialmobxAdminProperty
property (ie. the reaction) gets transferred from the old render-function to the new one.I favor the first solution since it doesn't add another level of indirection.
Thoughts?
The text was updated successfully, but these errors were encountered: