Skip to content
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

Better React 18 support #3590

Merged
merged 42 commits into from Mar 25, 2023
Merged

Conversation

urugator
Copy link
Collaborator

@urugator urugator commented Dec 17, 2022

mobx

It now keeps track of a global state version, which updates with each mutation.

mobx-react-lite

It now uses useSyncExternalStore, which should get rid of tearing (you have to update mobx, otherwise it should behave as previously).
Replaced reaction tracking utils with UniversalFinalizationRegistry. It works the same way, but I found it hard to orient myself in the original impl, so I rewrote it completely, hopefully for the better. It's also easier to reuse for class components.
Improved displayName/name handling as discussed in #3438.

mobx-react (class component)

Reactions of uncommited components are now correctly disposed. (fixes #3492)
Reactions don't notify uncommited components, avoiding the warning. (fixes #3492)
Removed symbol "polyfill" and replaced with actual Symbols.
Removed this.render replacement detection + warning. this.render is no longer configurable/writable (possibly BC *).
Reaction is no longer exposed as component[$mobx] (possibly BC *)
Component instance is no longer exposed as component[$mobx]["reactComponent"] (possibly BC *)
Deprecated disposeOnUnmount, it's not compatible with remounting.
Refactored code.
Fixed tests.

(*) BC for non-idiomatic usage or when depending on low level (private?) API.

I will update changeset once the changes settles.

@changeset-bot
Copy link

changeset-bot bot commented Dec 17, 2022

🦋 Changeset detected

Latest commit: e55c51c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
mobx Minor
mobx-react-lite Major
mobx-react Major

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

@@ -35,23 +37,7 @@ exports[`Redeclaring an existing observer component as an observer should log a
}
`;

exports[`SSR works #3448 1`] = `
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was not supposed to warn. Warning was caused by calling disableStaticRendering before unmount in the test. Therefore componentWillUnmount used logic for non-static rendering.

@@ -102,7 +102,7 @@ describe("inject based context", () => {
expect(C.displayName).toBe("inject(ComponentC)")
})

test.only("shouldn't change original displayName of component that uses forwardRef", () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was left in here for quite some time by mistake.

Comment on lines -118 to -127
/**
* If props are shallowly modified, react will render anyway,
* so atom.reportChanged() should not result in yet another re-render
*/
setHiddenProp(this, skipRenderKey, false)
/**
* forceUpdate will re-assign this.props. We don't want that to cause a loop,
* so detect these changes
*/
setHiddenProp(this, isForcingUpdateKey, false)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged these together into isUpdating as they basically handle the same thing:
One is set by reaction to prevent reportChanged on props setter,
second is set by props setter to prevent reaction.
So both says "I am already updating, do not cause another update"

Copy link
Member

@mweststrate mweststrate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good so far! I didn't review everything yet (still have to do the new observer implementations), but submitting this review already so that you can see the comments, as coming days I probably won't be able to review further with all Christmas activities :).

Merry Christmas and happy new year!

packages/mobx-react/src/disposeOnUnmount.ts Outdated Show resolved Hide resolved
packages/mobx/src/core/atom.ts Outdated Show resolved Hide resolved
"dependencies": {},
"dependencies": {
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"mobx": "^6.1.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

peer dependency probably has to be bumped to make sure globalState.stateVersion is available?

Copy link
Collaborator Author

@urugator urugator Dec 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that is pretty neat actually! I'd still bump the peerDependency (as the warnings are often ignored), and make this change itself a major version, since it might affect semantics and spreads risk a bit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is gonna be a major should it still be focused on React 18 support with otherwise minimal changes, or should this be an opportunity to introduce larger changes? (removing deprecated APIs, hooks, inject, options, cleanups, etc ...)

I assume the former.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do the former; decorators are also around the corner, an I expect after that is a better moment to do a bigger clean up (e.g. legacy decorator support etc)


test("state version updates correctly", () => {
// This test was designed around the idea of updating version only at the end of batch,
// which is NOT an implementation we've settled on, but the test is still valid.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds correct to me though :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean. Just to provide some background. I've been looking for a common place where I could update state version. endBatch (when nesting level gets back to zero) seemed like a good candidate, because it's actually called even for individual mutations, but I was worried about some edge cases, like autorun invoking itself. So I wrote this test and gave it a go. However I quickly realized endBatch is also often called without any actual mutation, therefore I threw the idea away and moved the version update to reportChanged, but I kept the test as is. I've left the comment to clarify why the test looks this way.

@mweststrate
Copy link
Member

mweststrate commented Dec 24, 2022 via email

},
"peerDependencies": {
"mobx": "^6.1.0",
"mobx": "^6.9.0",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A guess - I don't know the actual version it's gonna be released with. Dunno what's the correct way to handle it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot how to do that properly as well :-P But I think this should work anyway

Comment on lines +54 to +55
"mobx": "^6.8.0",
"mobx-react-lite": "^3.4.3",
Copy link
Collaborator Author

@urugator urugator Mar 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should become ^6.9?.0 and ^4.0.0 at the time of release. Again not sure how to handle that.

@@ -36,10 +36,10 @@
},
"homepage": "https://mobx.js.org",
"dependencies": {
"mobx-react-lite": "^3.4.0"
"mobx-react-lite": "^3.4.3"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should become ^4.0.0 at the time of release. Again not sure how to handle that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can already update it now, since in a local checkout it is symlinked anyway to the right disk location?

If that causes problems; I'm not sure anymore if this happened automatically or not when releasing a master, but otherwise we can immediately release it with a patch afterwards?

@urugator
Copy link
Collaborator Author

@mweststrate Please take a look at #3649 and let me know if I should merge it to master first and incorporate the change to this PR.

@urugator urugator marked this pull request as ready for review March 12, 2023 21:30
@mweststrate
Copy link
Member

Sorry for the delay! I'll dive into it this weekend

Copy link
Member

@mweststrate mweststrate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Imho it's okay to merge #3649 after this one.

packages/mobx-react/__tests__/observer.test.tsx Outdated Show resolved Hide resolved
@@ -36,10 +36,10 @@
},
"homepage": "https://mobx.js.org",
"dependencies": {
"mobx-react-lite": "^3.4.0"
"mobx-react-lite": "^3.4.3"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can already update it now, since in a local checkout it is symlinked anyway to the right disk location?

If that causes problems; I'm not sure anymore if this happened automatically or not when releasing a master, but otherwise we can immediately release it with a patch afterwards?

},
"peerDependencies": {
"mobx": "^6.1.0",
"mobx": "^6.9.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot how to do that properly as well :-P But I think this should work anyway

Copy link
Member

@mweststrate mweststrate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWESOME STUFF!

@ericmasiello
Copy link

ericmasiello commented Mar 21, 2023

I pulled this repo locally and configured it to use this branch.

I see mostly progress with 1 regression. In the output below, "mobx" is mobx 6.8.0 and mobx-react-lite 3.4.0. "mobx-beta" uses the latest from this branch.

The only regressions I see are:

  • Can interrupt render (time slicing)
  mobx
    With useTransition
      Level 1
        ✓ No tearing finally on update (4571 ms)
        ✓ No tearing finally on mount (5472 ms)
      Level 2
        ✕ No tearing temporarily on update (8641 ms)
        ✕ No tearing temporarily on mount (5460 ms)
      Level 3
        ✓ Can interrupt render (time slicing) (3586 ms)
        ✕ Can branch state (wip state) (2952 ms)
    With useDeferredValue
      Level 1
        ✓ No tearing finally on update (9622 ms)
        ✓ No tearing finally on mount (6486 ms)
      Level 2
        ✓ No tearing temporarily on update (14596 ms)
        ✕ No tearing temporarily on mount (5468 ms)
  mobx-beta
    With useTransition
      Level 1
        ✓ No tearing finally on update (7975 ms)
        ✓ No tearing finally on mount (4555 ms)
      Level 2
        ✓ No tearing temporarily on update (12924 ms)
        ✓ No tearing temporarily on mount (4514 ms)
      Level 3
        ✕ Can interrupt render (time slicing) (7911 ms)
        ✕ Can branch state (wip state) (6685 ms)
    With useDeferredValue
      Level 1
        ✓ No tearing finally on update (9647 ms)
        ✓ No tearing finally on mount (4535 ms)
      Level 2
        ✓ No tearing temporarily on update (14596 ms)
        ✓ No tearing temporarily on mount (4494 ms)

@urugator
Copy link
Collaborator Author

urugator commented Mar 21, 2023

@ericmasiello Cool, thank you for looking into it. Eventually I would love to see mobx as part of that repo. The results are what I was expecting/hoping for. I am not entirely sure about it, but I think level 3 is basically non-supportable by any kind of global state managment, because to do time slicing, you need to be able to split the state into indepedent pieces, which is impossible without any extra knowledge about the state. Eg in redux you would need two stores, two dispatchers etc, who knows absolutely nothing about one another. In mobx you would have to be able to somehow group observables, each group would maintain own version, but probably also own dependency tree, something like isolateGlobalState maybe? For state branching you probably need immutability.
The "regression" is most likely false positive on current version - without version checking, react is absolutely unware of observables, there is nothing that would tell him: No you can't do that, because it would cause some inconsistency in the output.

@ericmasiello
Copy link

ericmasiello commented Mar 22, 2023

@ericmasiello Besides mentioned fixed issues for which we do have tests, you should see improvements if you run it against: https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-rendering see this comment #2526 (comment) Personally I did not try and I don't have any setup for this, so at this point there is no easily reproducible evidence and you're welcome to change that :)

FYI, I have an open PR to add the latest release of mobx/mobx-react-lite to this repo dai-shi/will-this-react-global-state-work-in-concurrent-rendering#75

Correction: it merged!

@urugator urugator merged commit 44a2cf4 into mobxjs:main Mar 25, 2023
@github-actions github-actions bot mentioned this pull request Mar 25, 2023
@zhantx
Copy link

zhantx commented May 12, 2023

Hi @urugator
can you please elaborate why disposeOnUnmount is deprecated?
I tested it in react 18, it seems the function version is working fine if it's in didMount (so the reaction is recreated when remount)
Is it only the decorator version problematic in react 18? (I tested it too, it seems can't dispose the reaction properly)

Shall we only deprecate the decorator version?

@urugator
Copy link
Collaborator Author

urugator commented May 12, 2023

@zhantx As you said it would have to be called from componentDidMount:

  1. Substantially limits usability and the initial motivation for this utility is no longer fully realized.
  2. We would have to provide some means guarding against misuse (probably doable only partially and complicates things further).
  3. useEffect hook was designed to solve this disposer collocation problem. If the problem was not worth solving for class components by react team, it's probably also not worth solving by us.
  4. I think the general consensus at this point is to use reactions/autoruns sparingly and ideally keep them outside components, which presumably makes this utility less needed. Also just it's existence encourages the opposite.
  5. I think it's a niche utility that doesn't provide so much value. It doesn't hide much complexity, it's basically just a different syntax, imo not even drastically shorter.

EDIT: removed the last point as I realized it requires patching componenWillUnmount which I don't consider trivial.

@upsuper
Copy link
Contributor

upsuper commented May 12, 2023

Just want to mention that disposeOnUnmount is very useful for us, and it's widely used in our codebase. I'd also note that this utility doesn't encourage, nor does lack of it discourage the pattern. There exists lots of old code that simply collects disposers in componentDidMount and invokes them in componentWillUnmount. disposeOnUnmount helps simplify the code and ensure that they are disposed properly.

@mweststrate
Copy link
Member

mweststrate commented May 12, 2023 via email

@urugator
Copy link
Collaborator Author

urugator commented May 12, 2023

@upsuper
The utility was introduced specifically for disposing Mobx's resources - that is reaction/autorun, it would never exist otherwise.
Is it useful for other cases? Probably. Should Mobx be concerned about these. No. Mobx isn't toolkit for solving generic react problems.
So either it's for disposing autoruns/reactions, then it automaticaly raises the question why we expose a tool which makes it eaiser to use these in components, while at the same time discouraging from doing so.
Or it's not for disposing autoruns/reactions, then it's not a mobx problem and it shouldn't be part of this package (except BC).

That being said, as long as you're willing to be responsible for maintaing this tool, making sure it will be compatible with future react version (so we aren't in a similar situation year later), making sure it won't generate issues, complicate internals and people will use it correctly, I personally don't mind.

We've introduced some of these features in the past to be a bit more convinient here and there, to keep everyone happy and now we are reaping the fruits - we're dealing with React incompatibilities, piling up workarounds on the top of workarounds and introducing breaking changes.

@upsuper
Copy link
Contributor

upsuper commented May 12, 2023

I think it's fine that we maintain our own version of this utility in our codebase. We use it both for reaction/autorun and for other stuff following a similar pattern.

Mainly we want to understand the reason that it's deprecated, so that we know whether our use of it has any fundamental issue, in which case our own version of this utility would suffer the same problem as well.

It's good to know that it isn't that the utility really has anything incompatible with React.

@urugator
Copy link
Collaborator Author

A potential source of some trouble is the need to patch componenWillUnmount, which we already patch by https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/src/utils/utils.ts#L125
The complexity involved is beyond my current knowledge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

An exception is thrown when using Mobx6 and react18 in development mode
6 participants