Skip to content
This repository has been archived by the owner on Dec 31, 2020. It is now read-only.

Add test for proper reaction disposal in StrictMode #119

Merged
merged 7 commits into from
Apr 3, 2019
Merged

Add test for proper reaction disposal in StrictMode #119

merged 7 commits into from
Apr 3, 2019

Conversation

RoystonS
Copy link
Contributor

@RoystonS RoystonS commented Apr 2, 2019

This is a PR for a test to illustrate the issue with Concurrent Mode (and, more problematic, in StrictMode), as requested in #53 (comment)

(It currently fails, so you may not want to merge this right now...)

Copy link
Collaborator

@danielkcz danielkcz left a comment

Choose a reason for hiding this comment

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

I think I would rather prefer if you would be testing low-level useObserver since this adds few extras (like memo) in the mix. Would you be so kind to change it, please?

@@ -115,6 +115,52 @@ function runTestSuite(mode: "observer" | "useObserver") {
})
})

describe("double-rendering/StrictMode behavior", () => {
// tslint:disable: no-console
let originalConsoleError: typeof console.error
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use jest-console instead of this boilerplate :) It's even used in this test file already :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup. Will do. I don't normally use Jest, so I wasn't aware of that one.

@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 3, 2019

I think I would rather prefer if you would be testing low-level useObserver since this adds few extras (like memo) in the mix. Would you be so kind to change it, please?

I thought by putting my code inside the runTestSuite where it is, that the test would be run against both observer and useObserver?

@danielkcz
Copy link
Collaborator

I thought by putting my code inside the runTestSuite where it is, that the test would be run against both observer and useObserver?

Well, it's true, but you don't need to test observer really because it's a tiny wrapper around the useObserver. The "bug" we are testing against is within the useObserver so I vote for testing as close as possible to the actual problem occurrence, not the abstraction.

@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 3, 2019

Ok. I did originally start to create a separate useObserver.test.tsx file (as there wasn't one) but the realisation that nobody had done that before, and that there were combined tests for both useObserver and observer made me change it in order to follow the existing pattern instead.

I'll pull it out into the separate useObserver.test.tsx test file.

(I'm also playing with a fix; a small change to useObserver allows me to track whether the component has been committed or not before invoking forceUpdate, which fixes my strict mode issue; disposing stray reactions even if their observables haven't changed isn't very hard, but a little tricky to test depending on the schedulding implementation.)

I've tweaked the test name to reflect what it's really checking for.
N.B. Depending on the implementation of a fix, this test might need some
extra delays or act() calls to trigger a delayed cleanup before asserting that
all is clean.
@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 3, 2019

I've pulled it out into a separate file, and added a second test to verify not just that no console errors were emitted, but that the store is definitely unobserved after unmount.

@danielkcz
Copy link
Collaborator

danielkcz commented Apr 3, 2019

Awesome! Looks great. Now to figure a solution ... 🤔

It's most likely silly, but wouldn't something like this work? Can you please try it against tests?

    const [isMounted, setMounted] = React.useState(false)
    const reaction = useRef<Reaction | null>(null)
    if (!reaction.current) {
        reaction.current = new Reaction(`observer(${baseComponentName})`, () => {
            if (isMounted) {
              forceUpdate()
            }
        })
    }

    React.useEffect(() => {
      setMounted(true)
    }, [])

There is of course problem of lingering Reaction, but @mweststrate already mentioned some way of clearing those up globally, so that's perhaps not an issue to solve here.

@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 3, 2019

That'll probably work, but the setMounted call will trigger a second full render + commit for all observable components.

Using a ref achieves the same thing without the extra full round-trip:

    const committed = useRef(false)

    if (!reaction.current) {
        // First render for this component. Not yet committed.
        reaction.current = new Reaction(`observer(${baseComponentName})`, () => {
            if (committed.current) {
                forceUpdate()
            }
        })
    }

...
 
   // (Replaces the previous useUnmount call)
    useEffect(() => {
        committed.current = true
        return () => reaction.current!.dispose()
    }, [])

This does fix my StrictMode test. I'm just testing out some cleanup code that would dispose the stray reactions.

@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 3, 2019

I've removed my second (leaked observable) test for now: I'll move it into a separate PR as the fix for that is more complicated. I've also added a fix for the StrictMode warning issue, so it might now be possible to consider merging this.

@coveralls
Copy link

coveralls commented Apr 3, 2019

Coverage Status

Coverage decreased (-0.005%) to 99.301% when pulling d9f8fe5 on RoystonS:master into ed1634d on mobxjs:master.

@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 3, 2019

(Want me to remove useUnmount as I seem to have remove the last use of it...?)

@danielkcz
Copy link
Collaborator

Yes please, ditch useUnmount and it's indeed ready for merge. Thanks!

@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 3, 2019

useUnmount is no more.

@danielkcz danielkcz merged commit bf3a6c9 into mobxjs:master Apr 3, 2019
RoystonS pushed a commit to RoystonS/mobx-react-lite that referenced this pull request Apr 4, 2019
This is a test to check that observable changes made between the
first component render and commit are not lost.

It currently fails (and did so before the change in PR mobxjs#119)
@RoystonS
Copy link
Contributor Author

RoystonS commented Apr 9, 2019

I've been testing out this change in our codebase (with 100+ observer components) and it caused a breakage in one particular scenario linked to changes that occurred between render and first commit.

I'd already added an extra unit test in #121 to identify an existing issue where changes could be lost, and that test failed both before and after the #119 change. However, running the same test in non-strict mode produced a different behaviour: before #119 it passed, but after my #119 change, it fails, missing the change.

I see two options here:

  1. I could push here the extra, failing unit test (which I've already pushed into Schedule uncommitted reaction cleanup #121), and a fix I have (which involves another ref, checking if a change occurred between the render and commit, and forcing an update). That fixes the new unit test, and fixed the issue in our codebase, and stops changes during component setup being lost.
  2. We could back out the Add test for proper reaction disposal in StrictMode #119 change completely. That would leave strict mode noisy and with some changes during component setup lost.

danielkcz pushed a commit that referenced this pull request Apr 28, 2019
* Test cleaning up reactions for uncommitted components

* First attempt at a fix for cleaning up reactions from uncommitted components

* Use debounce instead of fixed interval

* Add unit test for missing observable changes before useEffect runs

This is a test to check that observable changes made between the
first component render and commit are not lost.

It currently fails (and did so before the change in PR #119)

* Add test for cleanup timer firing too early for some components

This demonstrates (in a slightly contrived way) how, if the
cleanup timer fires between a recent component being
rendered and it being committed, that it would incorrectly
tidy up a reaction for a soon-to-be-committed component.

* Update test for missing changes to check Strict and non-Strict mode

We had an existing test to check that observable changes between render
and commit didn't go missing, but it only checked in Strict mode, and
there's a problem with non-Strict mode.

* Add cleanup tracking and more tests

This adds full cleanup tracking, and even more tests:

- we now track how long ago potentially leaked reactions
were created, and only clean those that were leaked 'a while ago'
- if a reaction is incorrectly disposed because a component
went away for a very long time and came back again later
(in a way React doesn't even do right now), we safely recreate it and re-render
- trap the situation where a change is made to a tracked observable
between first render and commit (where we couldn't force an update
because we hadn't _been_ committed) and force a re-render
- more unit tests

* Fix renamed test file

When I renamed this file, I forgot the .test. suffix. D'oh.

* Extract tracking and cleanup logic out to separate file

* Update src/useObserver.ts

Co-Authored-By: RoystonS <[email protected]>

* Move some more tracking internals into the tracking code
danielkcz pushed a commit that referenced this pull request Apr 28, 2019
* Test cleaning up reactions for uncommitted components

* First attempt at a fix for cleaning up reactions from uncommitted components

* Use debounce instead of fixed interval

* Add unit test for missing observable changes before useEffect runs

This is a test to check that observable changes made between the
first component render and commit are not lost.

It currently fails (and did so before the change in PR #119)

* Add test for cleanup timer firing too early for some components

This demonstrates (in a slightly contrived way) how, if the
cleanup timer fires between a recent component being
rendered and it being committed, that it would incorrectly
tidy up a reaction for a soon-to-be-committed component.

* Update test for missing changes to check Strict and non-Strict mode

We had an existing test to check that observable changes between render
and commit didn't go missing, but it only checked in Strict mode, and
there's a problem with non-Strict mode.

* Add cleanup tracking and more tests

This adds full cleanup tracking, and even more tests:

- we now track how long ago potentially leaked reactions
were created, and only clean those that were leaked 'a while ago'
- if a reaction is incorrectly disposed because a component
went away for a very long time and came back again later
(in a way React doesn't even do right now), we safely recreate it and re-render
- trap the situation where a change is made to a tracked observable
between first render and commit (where we couldn't force an update
because we hadn't _been_ committed) and force a re-render
- more unit tests

* Fix renamed test file

When I renamed this file, I forgot the .test. suffix. D'oh.

* Extract tracking and cleanup logic out to separate file

* Update src/useObserver.ts

Co-Authored-By: RoystonS <[email protected]>

* Move some more tracking internals into the tracking code
danielkcz pushed a commit that referenced this pull request Apr 28, 2019
* Test cleaning up reactions for uncommitted components

* First attempt at a fix for cleaning up reactions from uncommitted components

* Use debounce instead of fixed interval

* Add unit test for missing observable changes before useEffect runs

This is a test to check that observable changes made between the
first component render and commit are not lost.

It currently fails (and did so before the change in PR #119)

* Add test for cleanup timer firing too early for some components

This demonstrates (in a slightly contrived way) how, if the
cleanup timer fires between a recent component being
rendered and it being committed, that it would incorrectly
tidy up a reaction for a soon-to-be-committed component.

* Update test for missing changes to check Strict and non-Strict mode

We had an existing test to check that observable changes between render
and commit didn't go missing, but it only checked in Strict mode, and
there's a problem with non-Strict mode.

* Add cleanup tracking and more tests

This adds full cleanup tracking, and even more tests:

- we now track how long ago potentially leaked reactions
were created, and only clean those that were leaked 'a while ago'
- if a reaction is incorrectly disposed because a component
went away for a very long time and came back again later
(in a way React doesn't even do right now), we safely recreate it and re-render
- trap the situation where a change is made to a tracked observable
between first render and commit (where we couldn't force an update
because we hadn't _been_ committed) and force a re-render
- more unit tests

* Fix renamed test file

When I renamed this file, I forgot the .test. suffix. D'oh.

* Extract tracking and cleanup logic out to separate file

* Update src/useObserver.ts

Co-Authored-By: RoystonS <[email protected]>

* Move some more tracking internals into the tracking code
danielkcz pushed a commit that referenced this pull request May 1, 2019
danielkcz pushed a commit that referenced this pull request May 1, 2019
danielkcz pushed a commit that referenced this pull request Apr 6, 2020
* Schedule uncommitted reaction cleanup (#121)

* Test cleaning up reactions for uncommitted components

* First attempt at a fix for cleaning up reactions from uncommitted components

* Use debounce instead of fixed interval

* Add unit test for missing observable changes before useEffect runs

This is a test to check that observable changes made between the
first component render and commit are not lost.

It currently fails (and did so before the change in PR #119)

* Add test for cleanup timer firing too early for some components

This demonstrates (in a slightly contrived way) how, if the
cleanup timer fires between a recent component being
rendered and it being committed, that it would incorrectly
tidy up a reaction for a soon-to-be-committed component.

* Update test for missing changes to check Strict and non-Strict mode

We had an existing test to check that observable changes between render
and commit didn't go missing, but it only checked in Strict mode, and
there's a problem with non-Strict mode.

* Add cleanup tracking and more tests

This adds full cleanup tracking, and even more tests:

- we now track how long ago potentially leaked reactions
were created, and only clean those that were leaked 'a while ago'
- if a reaction is incorrectly disposed because a component
went away for a very long time and came back again later
(in a way React doesn't even do right now), we safely recreate it and re-render
- trap the situation where a change is made to a tracked observable
between first render and commit (where we couldn't force an update
because we hadn't _been_ committed) and force a re-render
- more unit tests

* Fix renamed test file

When I renamed this file, I forgot the .test. suffix. D'oh.

* Extract tracking and cleanup logic out to separate file

* Update src/useObserver.ts

Co-Authored-By: RoystonS <[email protected]>

* Move some more tracking internals into the tracking code

* 2.0.0-alpha.0

* 2.0.0-alpha.1

* 2.0.0-alpha.2

* Upgrade to React 16.9

* Add dedup script and run it

* Remove deprecated hooks

* Increase size limit

* Remove note about Next version

* Remove unused productionMode util

* Improve readme

* Pin dependencies

* Merge master properly

* Ignore build cache files

* Revert removal of tsdx dep

* Remove .browserlistrc

* 2.0.0-alpha.3

* Bundling need to use build tsconfig

* 2.0.0-alpha.4

* Remove object destructuring from optimizeForReactDOM/Native (#240)

* Preserve generics when using `observer` (#244)

* Preserve generics when using `observer`

* Remove any casting from statics as it's no longer needed

* Re-add overloads for explicitly specifying props type

* Allow for passing options without `forwardRef`

* Merge new `observer` overloads

* Remove copy of UMD bundle

* 2.0.0-alpha.5

* Batched updates are mandatory

* Replace .npmignore with files field

* Fix tests

* Increase size limit

Co-authored-by: Royston Shufflebotham <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-authored-by: Tarvo R <[email protected]>
Co-authored-by: Lukáš Novotný <[email protected]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants