diff --git a/package.json b/package.json
index 53a43cb5..43ab162c 100644
--- a/package.json
+++ b/package.json
@@ -55,8 +55,8 @@
"cross-env": "^6.0.0",
"kcd-scripts": "^1.7.0",
"npm-run-all": "^4.1.5",
- "react": "^16.9.0",
- "react-dom": "^16.9.0",
+ "react": "^0.0.0-experimental-f6b8d31a7",
+ "react-dom": "^0.0.0-experimental-f6b8d31a7",
"rimraf": "^3.0.0"
},
"peerDependencies": {
diff --git a/src/__tests__/concurrent.js b/src/__tests__/concurrent.js
new file mode 100644
index 00000000..a70dd237
--- /dev/null
+++ b/src/__tests__/concurrent.js
@@ -0,0 +1,52 @@
+import React from 'react'
+import {act, render, cleanup} from '../'
+
+test('simple render works like legacy', () => {
+ const {container} = render(
test
, {root: 'concurrent'})
+
+ expect(container).toHaveTextContent('test')
+})
+
+test('unmounts are flushed in sync', () => {
+ const {container, unmount} = render(test
, {root: 'concurrent'})
+
+ unmount()
+
+ expect(container.children).toHaveLength(0)
+})
+
+test('rerender are flushed in sync', () => {
+ const {container, rerender} = render(foo
, {root: 'concurrent'})
+
+ rerender(bar
)
+
+ expect(container).toHaveTextContent('foo')
+})
+
+test('cleanup unmounts in sync', () => {
+ const {container} = render(test
, {root: 'concurrent'})
+
+ cleanup()
+
+ expect(container.children).toHaveLength(0)
+})
+
+test('state updates are concurrent', () => {
+ function TrackingButton() {
+ const [clickCount, increment] = React.useReducer(n => n + 1, 0)
+
+ return (
+
+ )
+ }
+ const {getByRole} = render(, {root: 'concurrent'})
+
+ act(() => {
+ getByRole('button').click()
+ expect(getByRole('button')).toHaveTextContent('Clicked 0 times')
+ })
+
+ expect(getByRole('button')).toHaveTextContent('Clicked 1 times')
+})
diff --git a/src/pure.js b/src/pure.js
index 1b1838bf..9583f8ab 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -19,6 +19,10 @@ configureDTL({
})
const mountedContainers = new Set()
+/**
+ * @type {WeakMap}
+ */
+const mountedRoots = new WeakMap()
function render(
ui,
@@ -28,6 +32,7 @@ function render(
queries,
hydrate = false,
wrapper: WrapperComponent,
+ root,
} = {},
) {
if (!baseElement) {
@@ -44,13 +49,21 @@ function render(
// they're passing us a custom container or not.
mountedContainers.add(container)
+ let reactRoot = null
+ if (root === 'concurrent') {
+ reactRoot = ReactDOM.createRoot(container, {hydrate})
+ mountedRoots.set(container, reactRoot)
+ }
+
const wrapUiIfNeeded = innerElement =>
WrapperComponent
? React.createElement(WrapperComponent, null, innerElement)
: innerElement
act(() => {
- if (hydrate) {
+ if (reactRoot) {
+ reactRoot.render(wrapUiIfNeeded(ui))
+ } else if (hydrate) {
ReactDOM.hydrate(wrapUiIfNeeded(ui), container)
} else {
ReactDOM.render(wrapUiIfNeeded(ui), container)
@@ -66,11 +79,23 @@ function render(
el.forEach(e => console.log(prettyDOM(e)))
: // eslint-disable-next-line no-console,
console.log(prettyDOM(el)),
- unmount: () => ReactDOM.unmountComponentAtNode(container),
+ unmount: () => {
+ if (reactRoot) {
+ act(() => {
+ reactRoot.unmount()
+ })
+ } else {
+ ReactDOM.unmountComponentAtNode(container)
+ }
+ },
rerender: rerenderUi => {
- render(wrapUiIfNeeded(rerenderUi), {container, baseElement})
- // 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
+ if (reactRoot === null) {
+ render(wrapUiIfNeeded(rerenderUi), {container, baseElement})
+ // 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
+ } else {
+ reactRoot.render(wrapUiIfNeeded(rerenderUi))
+ }
},
asFragment: () => {
/* istanbul ignore if (jsdom limitation) */
@@ -95,7 +120,15 @@ function cleanup() {
// maybe one day we'll expose this (perhaps even as a utility returned by render).
// but let's wait until someone asks for it.
function cleanupAtContainer(container) {
- ReactDOM.unmountComponentAtNode(container)
+ const reactRoot = mountedRoots.get(container)
+ if (reactRoot === undefined) {
+ ReactDOM.unmountComponentAtNode(container)
+ } else {
+ act(() => {
+ reactRoot.unmount()
+ })
+ }
+
if (container.parentNode === document.body) {
document.body.removeChild(container)
}
diff --git a/tests/setup-env.js b/tests/setup-env.js
index 264828a9..442d82bd 100644
--- a/tests/setup-env.js
+++ b/tests/setup-env.js
@@ -1 +1,3 @@
import '@testing-library/jest-dom/extend-expect'
+// eslint-disable-next-line import/no-extraneous-dependencies
+jest.mock('scheduler', () => require('scheduler/unstable_mock'))