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

feat: Use concurrent React when available #925

Closed
wants to merge 14 commits into from

Conversation

eps1lon
Copy link
Member

@eps1lon eps1lon commented Jun 8, 2021

TODO:

  • how to tell the coverage report that we've reached full coverage across the whole build matrix i.e. with different versions of React?
    By telling codecov that different React versions act as feature flags

Revisiting #507

BREAKING CHANGE: Concurrent features will be used by default if supported by the installed version of react-dom. To opt-out use `render(element, { legacyRoot: true }).

What:

Closes #509

Why:

Concurrent features will be available in React 18.

Opt-out (breaking change) vs opt-in (feature release):
createRoot is considered to be the default in React 18 so we should follow.

How:

Add a concurrent option to render. Note that there are still some unsolved problems with testing concurrent features like reactwg/react-18#21 (comment) and reactwg/react-18#23 (comment). So testing concurrent features will change. We will only support the latest pre-release of React 18. So any release of @testing-library/react may break your testing setup if don't have the latest alpha/beta/release candidate installed.

Checklist:

  • Documentation added to the
    docs site
  • Tests
  • Typescript definitions updated
  • Ready to be merged

@eps1lon eps1lon added enhancement New feature or request BREAKING CHANGE This change will require a major version bump labels Jun 8, 2021
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jun 8, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 2ac2377:

Sandbox Source
React Configuration
react-testing-library-examples Configuration

@eps1lon eps1lon changed the base branch from main to alpha June 8, 2021 17:49
@eps1lon eps1lon force-pushed the feat/concurrent-render branch from 8910fa2 to c83e9c1 Compare June 8, 2021 18:05
@codecov
Copy link

codecov bot commented Jun 8, 2021

Codecov Report

Merging #925 (f570722) into alpha (05c7421) will decrease coverage by 5.08%.
The diff coverage is 85.71%.

❗ Current head f570722 differs from pull request most recent head 2ac2377. Consider uploading reports for the commit 2ac2377 to get more accurate results
Impacted file tree graph

@@             Coverage Diff             @@
##             alpha     #925      +/-   ##
===========================================
- Coverage   100.00%   94.91%   -5.09%     
===========================================
  Files            4        4              
  Lines          140      177      +37     
  Branches        28       34       +6     
===========================================
+ Hits           140      168      +28     
- Misses           0        8       +8     
- Partials         0        1       +1     
Flag Coverage Δ
latest 94.91% <85.71%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
src/pure.js 89.41% <85.71%> (-10.59%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 05c7421...2ac2377. Read the comment docs.

@eps1lon eps1lon marked this pull request as ready for review June 8, 2021 19:32
@eps1lon eps1lon requested a review from kentcdodds June 8, 2021 19:34
@kentcdodds
Copy link
Member

how to tell the coverage report that we've reached full coverage across the whole build matrix i.e. with different versions of React?

I believe that codecov can combine coverage reports, but I don't think there's a reasonable way to make Jest happy about our coverage number in a single environment 😬

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

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

Thanks for your work on this and for your work in the working group! I have a few questions.

}

let root
// eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
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 the rule is only complaining about the fact that this code could be written like this:

if (mountedContainers.has(container)) {
  // the forEach here
} else {
  // create the root impl here.
}

Copy link
Member Author

Choose a reason for hiding this comment

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

This was my interpretation as well. But I wanted to map the code to the lifetime of the root/container tuple as well:

  1. gets created
  2. gets reused

ESLint is telling me to put the "gets reused" branch first which is not what happens in time. So if you're reading the code top-down you don't know where the root/container tuple is coming from.

I should rewrite the comment since I tried to explain the reason there.

Basically, I don't think no-negated-condition does anything useful. I could just as well write

const needsRoot = !mountedContainers.has(container)
if (needsRoot) {
  // create the root impl here.
} else {
  // the forEach here
}

which means I have to name one more thing but then I don't have to use the noisy eslint-disable with confusing comments.

Comment on lines +195 to +213
function waitFor(callback, options) {
return waitForDTL(() => {
let result
act(() => {
result = callback()
})
return result
}, options)
}

function waitForElementToBeRemoved(callback, options) {
return waitForElementToBeRemovedDTL(() => {
let result
act(() => {
result = callback()
})
return result
}, options)
Copy link
Member

Choose a reason for hiding this comment

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

I take it that these are added here because the removal of asyncWrapper?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, updates aren't flushed until we exit the outermost act. asyncWrapper was wrapping the whole waitFor call so the screen didn't ever get updated.

I'm not sure if we shouldn't actually change the dom-testing-library implementation so that we end up with

act(() => {
  jest.advanceTimersByTime(interval)
})

or fork it entirely.

And we'd still have the issue of real timers: reactwg/react-18#23 (comment)

Comment on lines -12 to -18
asyncWrapper: async cb => {
let result
await asyncAct(async () => {
result = await cb()
})
return result
},
Copy link
Member

Choose a reason for hiding this comment

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

So to be clear, there's no longer the concept of an async act anymore?

Copy link
Member

Choose a reason for hiding this comment

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

As far as I understand from reactwg/react-18#21 (comment), async act still exists in React 18, but sync act won't use microtasks so you won't need to flush them with await.

Copy link
Member Author

Choose a reason for hiding this comment

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

So to be clear, there's no longer the concept of an async act anymore?

Nothing changed in that regard.

I needed to remove to wrapper because it was used to wrap all the polling in waitFor:

await ReactDOMTestUtils.act(async () => {
  await new Promise((resolve) => {
    setInterval(async () => {
      if (
        // That's the condition we wait for
        document.querySelector("main").getAttribute("aria-busy") !== "true"
      ) {
        resolve();
      }
    }, 50);
  });
});

and this meant that the screen isn't actually updated during the polling i.e. it'll always reject and then the screen gets updated. This may be fine since we could just check one more time after the timeout but then interval becomes useless and the function runtime is always equal to timeout.

tests/setup-env.js Show resolved Hide resolved
@nickserv
Copy link
Member

nickserv commented Jun 8, 2021

Since React 18 is publishing new releases nightly to both the next and experimental tags, should we only use one of those two tags for now (to save CI resources as they're both the same builds of React 18 alpha) and run daily scheduled builds (so we can catch regressions in React alphas even if there isn't a commit to Testing Library)? I can try this in a separate PR if you'd like.

Comment on lines -12 to -18
asyncWrapper: async cb => {
let result
await asyncAct(async () => {
result = await cb()
})
return result
},
Copy link
Member

Choose a reason for hiding this comment

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

As far as I understand from reactwg/react-18#21 (comment), async act still exists in React 18, but sync act won't use microtasks so you won't need to flush them with await.

@@ -16,6 +16,7 @@ jobs:
# ignore all-contributors PRs
if: ${{ !contains(github.head_ref, 'all-contributors') }}
strategy:
fail-fast: false
Copy link
Member

@nickserv nickserv Jun 8, 2021

Choose a reason for hiding this comment

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

Maybe this should only be enabled in next/experimental React tag builds to keep latest tag builds fast.

Copy link
Member Author

Choose a reason for hiding this comment

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

I would land this change regardless of this PR. Personally, I'd like to know if a job only fails in one job. Otherwise how do I know what's causing the issue?

src/pure.js Show resolved Hide resolved
{
container,
baseElement = container,
legacyRoot = typeof ReactDOM.createRoot !== 'function',
Copy link
Member

Choose a reason for hiding this comment

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

createRoot is considered to be the default in React 18 so we should follow.

Defaulting to using createRoot could cause unexpected test failures for users that upgrade from React 17 to 18, but are still using the legacy render function for some reason (such as wanting to keep dependencies up to date without using concurrent mode or A/B testing with and without concurrent mode). I think it may be safer to leave it off, and have it be an intentional upgrade (like replacing render with createRoot in React itself). Then in the future if legacy render is removed, we could remove the option (replacing it with the current conditional check). That being said, I'm not sure how common these differences in behavior would be, and it could help to test it out with existing React 17 apps first.

Copy link
Member Author

Choose a reason for hiding this comment

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

Defaulting to using createRoot could cause unexpected test failures for users that upgrade from React 17 to 18

I think this risk will be eventually very low. In the end this PR will land in a major release so breakage should be expected. I just don't think it's healthy if React considers one API the default while we consider another one the default. Ultimately you can always wrap our render with a different default value. In jest you could just mock it which is what I did originally: https://github.com/eps1lon/react-testing-library/blob/04e865c0009a0d709a76846c80cdda683ac4b0fc/src/__mocks__/pure.js

Copy link
Member Author

Choose a reason for hiding this comment

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

Small update: React now logs a deprecation warning in React 18 if you use ReactDOM.render or ReactDOM.hydrate. I think we should not trigger warnings by default so the "createRoot by default" approach is going in the right direciton in my opinion.

@eps1lon

This comment has been minimized.

@eps1lon
Copy link
Member Author

eps1lon commented Jun 9, 2021

Since React 18 is publishing new releases nightly to both the next and experimental tags, should we only use one of those two tags for now (to save CI resources as they're both the same builds of React 18 alpha) and run daily scheduled builds (so we can catch regressions in React alphas even if there isn't a commit to Testing Library)?

As far as I know experimental and next release channels will stay so I wouldn't want to change something temporarily. Especially since I'm not aware of any CI ressource problems we have.

@eps1lon

This comment has been minimized.

@nickserv
Copy link
Member

nickserv commented Jun 9, 2021

Since React 18 is publishing new releases nightly to both the next and experimental tags, should we only use one of those two tags for now (to save CI resources as they're both the same builds of React 18 alpha) and run daily scheduled builds (so we can catch regressions in React alphas even if there isn't a commit to Testing Library)?

As far as I know experimental and next release channels will stay so I wouldn't want to change something temporarily. Especially since I'm not aware of any CI ressource problems we have.

What do you think about adding scheduled events for daily builds (without changing the tags)?

@kentcdodds
Copy link
Member

FYI, I'm going out of town for a week so don't hold this up on my account.

@eps1lon
Copy link
Member Author

eps1lon commented Jun 12, 2021

FYI, I'm going out of town for a week so don't hold this up on my account.

The reason I'm hesitant is that waitFor and findBy doesn't really work anymore with React 18. I'm hoping this gets answered soon but I'd rather release the current state of alpha first and defer this PR to another major release.

@eps1lon eps1lon closed this Jun 23, 2021
@eps1lon eps1lon deleted the branch testing-library:alpha June 23, 2021 06:32
@eps1lon
Copy link
Member Author

eps1lon commented Jun 23, 2021

No github the branch is there again so why can't I re-open this? Well thanks for loosing context. Continued in #937

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BREAKING CHANGE This change will require a major version bump enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for React 18
3 participants