diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index f1359d76..7094024e 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -30,7 +30,7 @@ jobs:
fail-fast: false
matrix:
node: [14, 16, 18]
- react: [latest, canary, experimental]
+ react: ['18.x', latest, canary, experimental]
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 00000000..73bf0099
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,16 @@
+const {jest: jestConfig} = require('kcd-scripts/config')
+
+module.exports = Object.assign(jestConfig, {
+ coverageThreshold: {
+ ...jestConfig.coverageThreshold,
+ // Full coverage across the build matrix (React 18, 19) but not in a single job
+ // Ful coverage is checked via codecov
+ './src/act-compat.js': {
+ // minimum coverage of jobs using React 18 and 19
+ branches: 90,
+ functions: 100,
+ lines: 100,
+ statements: 100,
+ },
+ },
+})
diff --git a/package.json b/package.json
index 70aebdad..05ad229c 100644
--- a/package.json
+++ b/package.json
@@ -62,8 +62,8 @@
"typescript": "^4.1.2"
},
"peerDependencies": {
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
+ "react": "^18.0.0 || ^18.3.0-canary",
+ "react-dom": "^18.0.0 || ^18.3.0-canary"
},
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js
index 0412a8a3..0464ad24 100644
--- a/src/__tests__/new-act.js
+++ b/src/__tests__/new-act.js
@@ -1,10 +1,13 @@
let asyncAct
-jest.mock('react-dom/test-utils', () => ({
- act: cb => {
- return cb()
- },
-}))
+jest.mock('react', () => {
+ return {
+ ...jest.requireActual('react'),
+ act: cb => {
+ return cb()
+ },
+ }
+})
beforeEach(() => {
jest.resetModules()
diff --git a/src/__tests__/render.js b/src/__tests__/render.js
index 39f4bc92..0c797460 100644
--- a/src/__tests__/render.js
+++ b/src/__tests__/render.js
@@ -212,29 +212,4 @@ describe('render API', () => {
expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1)
})
-
- test('legacyRoot uses legacy ReactDOM.render', () => {
- expect(() => {
- render(
, {legacyRoot: true})
- }).toErrorDev(
- [
- "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
- ],
- {withoutStack: true},
- )
- })
-
- test('legacyRoot uses legacy ReactDOM.hydrate', () => {
- const ui =
- const container = document.createElement('div')
- container.innerHTML = ReactDOMServer.renderToString(ui)
- expect(() => {
- render(ui, {container, hydrate: true, legacyRoot: true})
- }).toErrorDev(
- [
- "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
- ],
- {withoutStack: true},
- )
- })
})
diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js
index 11b7009a..66843b45 100644
--- a/src/__tests__/renderHook.js
+++ b/src/__tests__/renderHook.js
@@ -60,28 +60,3 @@ test('allows wrapper components', async () => {
expect(result.current).toEqual('provided')
})
-
-test('legacyRoot uses legacy ReactDOM.render', () => {
- const Context = React.createContext('default')
- function Wrapper({children}) {
- return {children}
- }
- let result
- expect(() => {
- result = renderHook(
- () => {
- return React.useContext(Context)
- },
- {
- wrapper: Wrapper,
- legacyRoot: true,
- },
- ).result
- }).toErrorDev(
- [
- "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot",
- ],
- {withoutStack: true},
- )
- expect(result.current).toEqual('provided')
-})
diff --git a/src/act-compat.js b/src/act-compat.js
index 86518196..5877755c 100644
--- a/src/act-compat.js
+++ b/src/act-compat.js
@@ -1,6 +1,7 @@
-import * as testUtils from 'react-dom/test-utils'
+import * as React from 'react'
+import * as DeprecatedReactTestUtils from 'react-dom/test-utils'
-const domAct = testUtils.act
+const reactAct = React.act ?? DeprecatedReactTestUtils.act
function getGlobalThis() {
/* istanbul ignore else */
@@ -78,7 +79,7 @@ function withGlobalActEnvironment(actImplementation) {
}
}
-const act = withGlobalActEnvironment(domAct)
+const act = withGlobalActEnvironment(reactAct)
export default act
export {
diff --git a/src/pure.js b/src/pure.js
index 3939a11a..feb7d5f5 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -1,5 +1,4 @@
import * as React from 'react'
-import ReactDOM from 'react-dom'
import * as ReactDOMClient from 'react-dom/client'
import {
getQueriesForElement,
@@ -73,7 +72,7 @@ configureDTL({
*/
const mountedContainers = new Set()
/**
- * @type Array<{container: import('react-dom').Container, root: ReturnType}>
+ * @type Array<{container: import('react-dom').Container, root: ReturnType}>
*/
const mountedRootEntries = []
@@ -89,73 +88,10 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) {
: innerElement
}
-function createConcurrentRoot(
- container,
- {hydrate, ui, wrapper: WrapperComponent},
-) {
- let root
- if (hydrate) {
- act(() => {
- root = ReactDOMClient.hydrateRoot(
- container,
- strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
- )
- })
- } else {
- root = ReactDOMClient.createRoot(container)
- }
-
- return {
- hydrate() {
- /* istanbul ignore if */
- if (!hydrate) {
- throw new Error(
- 'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
- )
- }
- // Nothing to do since hydration happens when creating the root object.
- },
- render(element) {
- root.render(element)
- },
- unmount() {
- root.unmount()
- },
- }
-}
-
-function createLegacyRoot(container) {
- return {
- hydrate(element) {
- ReactDOM.hydrate(element, container)
- },
- render(element) {
- ReactDOM.render(element, container)
- },
- unmount() {
- ReactDOM.unmountComponentAtNode(container)
- },
- }
-}
-
-function renderRoot(
+function createTestView(
ui,
- {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
+ {baseElement, container, queries, root, WrapperComponent},
) {
- act(() => {
- if (hydrate) {
- root.hydrate(
- strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
- container,
- )
- } else {
- root.render(
- strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
- container,
- )
- }
- })
-
return {
container,
baseElement,
@@ -171,11 +107,10 @@ function renderRoot(
})
},
rerender: rerenderUi => {
- renderRoot(rerenderUi, {
- container,
- baseElement,
- root,
- wrapper: WrapperComponent,
+ act(() => {
+ root.render(
+ strictModeIfNeeded(wrapUiIfNeeded(rerenderUi, WrapperComponent)),
+ )
})
// Intentionally do not return anything to avoid unnecessarily complicating the API.
// folks can use all the same utilities we return in the first place that are bound to the container
@@ -201,10 +136,9 @@ function render(
{
container,
baseElement = container,
- legacyRoot = false,
queries,
hydrate = false,
- wrapper,
+ wrapper: WrapperComponent,
} = {},
) {
if (!baseElement) {
@@ -219,8 +153,16 @@ function render(
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.
if (!mountedContainers.has(container)) {
- const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
- root = createRootImpl(container, {hydrate, ui, wrapper})
+ if (hydrate) {
+ act(() => {
+ root = ReactDOMClient.hydrateRoot(
+ container,
+ strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
+ )
+ })
+ } else {
+ root = ReactDOMClient.createRoot(container)
+ }
mountedRootEntries.push({container, root})
// we'll add it to the mounted containers regardless of whether it's actually
@@ -238,12 +180,17 @@ function render(
})
}
- return renderRoot(ui, {
+ if (!hydrate) {
+ act(() => {
+ root.render(strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)))
+ })
+ }
+
+ return createTestView(ui, {
container,
baseElement,
queries,
- hydrate,
- wrapper,
+ WrapperComponent,
root,
})
}
diff --git a/tests/shouldIgnoreConsoleError.js b/tests/shouldIgnoreConsoleError.js
index 75528267..a17c4f85 100644
--- a/tests/shouldIgnoreConsoleError.js
+++ b/tests/shouldIgnoreConsoleError.js
@@ -36,6 +36,7 @@ module.exports = function shouldIgnoreConsoleError(format) {
// Ignore it too.
return true
}
+ // TODO: Suppress deprecation warning from react-dom/test-utils
}
}
// Looks legit
diff --git a/types/index.d.ts b/types/index.d.ts
index 1f1135c5..e621a8e6 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -67,17 +67,12 @@ export interface RenderOptions<
*/
baseElement?: BaseElement
/**
- * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
- * rendering and use ReactDOM.hydrate to mount your components.
+ * If `hydrate` is set to `true`, then it will create a root with `ReactDOMClient.hydrateRoot`. This may be useful if you are using server-side
+ * rendering and use ReactDOMClient.hydrateRoot to mount your components.
*
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
*/
hydrate?: boolean
- /**
- * Set to `true` if you want to force synchronous `ReactDOM.render`.
- * Otherwise `render` will default to concurrent React if available.
- */
- legacyRoot?: boolean
/**
* Queries to bind. Overrides the default set from DOM Testing Library unless merged.
*