-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Conversation
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:
|
8910fa2
to
c83e9c1
Compare
Codecov Report
@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
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 😬 |
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 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. |
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 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.
}
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 was my interpretation as well. But I wanted to map the code to the lifetime of the root/container tuple as well:
- gets created
- 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.
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) |
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 take it that these are added here because the removal of asyncWrapper
?
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.
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)
asyncWrapper: async cb => { | ||
let result | ||
await asyncAct(async () => { | ||
result = await cb() | ||
}) | ||
return result | ||
}, |
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 to be clear, there's no longer the concept of an async
act anymore?
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.
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
.
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 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
.
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. |
asyncWrapper: async cb => { | ||
let result | ||
await asyncAct(async () => { | ||
result = await cb() | ||
}) | ||
return result | ||
}, |
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.
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 |
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.
Maybe this should only be enabled in next/experimental React tag builds to keep latest tag builds fast.
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 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?
{ | ||
container, | ||
baseElement = container, | ||
legacyRoot = typeof ReactDOM.createRoot !== 'function', |
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.
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.
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.
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
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.
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.
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
What do you think about adding scheduled events for daily builds (without changing the tags)? |
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 |
Otherwise we can't see directly which version is failing
These only exists as invariants.
0a52d49
to
f570722
Compare
No github the branch is there again so why can't I re-open this? Well thanks for loosing context. Continued in #937 |
TODO:
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 torender
. 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:
docs site