diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
index 75fca79208348..532030d0b262a 100644
--- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
@@ -43,35 +43,6 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('');
});
- it('`root.render` returns a thenable work object', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const work = root.render('Hi');
- let ops = [];
- work.then(() => {
- ops.push('inside callback: ' + container.textContent);
- });
- ops.push('before committing: ' + container.textContent);
- Scheduler.unstable_flushAll();
- ops.push('after committing: ' + container.textContent);
- expect(ops).toEqual([
- 'before committing: ',
- // `then` callback should fire during commit phase
- 'inside callback: Hi',
- 'after committing: Hi',
- ]);
- });
-
- it('resolves `work.then` callback synchronously if the work already committed', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const work = root.render('Hi');
- Scheduler.unstable_flushAll();
- let ops = [];
- work.then(() => {
- ops.push('inside callback');
- });
- expect(ops).toEqual(['inside callback']);
- });
-
it('supports hydration', async () => {
const markup = await new Promise(resolve =>
resolve(
@@ -129,200 +100,6 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('abdc');
});
- it('can defer a commit by batching it', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const batch = root.createBatch();
- batch.render(
Hi
);
- // Hasn't committed yet
- expect(container.textContent).toEqual('');
- // Commit
- batch.commit();
- expect(container.textContent).toEqual('Hi');
- });
-
- it('applies setState in componentDidMount synchronously in a batch', done => {
- class App extends React.Component {
- state = {mounted: false};
- componentDidMount() {
- this.setState({
- mounted: true,
- });
- }
- render() {
- return this.state.mounted ? 'Hi' : 'Bye';
- }
- }
-
- const root = ReactDOM.unstable_createRoot(container);
- const batch = root.createBatch();
- batch.render();
-
- Scheduler.unstable_flushAll();
-
- // Hasn't updated yet
- expect(container.textContent).toEqual('');
-
- let ops = [];
- batch.then(() => {
- // Still hasn't updated
- ops.push(container.textContent);
-
- // Should synchronously commit
- batch.commit();
- ops.push(container.textContent);
-
- expect(ops).toEqual(['', 'Hi']);
- done();
- });
- });
-
- it('does not restart a completed batch when committing if there were no intervening updates', () => {
- let ops = [];
- function Foo(props) {
- ops.push('Foo');
- return props.children;
- }
- const root = ReactDOM.unstable_createRoot(container);
- const batch = root.createBatch();
- batch.render(Hi);
- // Flush all async work.
- Scheduler.unstable_flushAll();
- // Root should complete without committing.
- expect(ops).toEqual(['Foo']);
- expect(container.textContent).toEqual('');
-
- ops = [];
-
- // Commit. Shouldn't re-render Foo.
- batch.commit();
- expect(ops).toEqual([]);
- expect(container.textContent).toEqual('Hi');
- });
-
- it('can wait for a batch to finish', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const batch = root.createBatch();
- batch.render('Foo');
-
- Scheduler.unstable_flushAll();
-
- // Hasn't updated yet
- expect(container.textContent).toEqual('');
-
- let ops = [];
- batch.then(() => {
- // Still hasn't updated
- ops.push(container.textContent);
- // Should synchronously commit
- batch.commit();
- ops.push(container.textContent);
- });
-
- expect(ops).toEqual(['', 'Foo']);
- });
-
- it('`batch.render` returns a thenable work object', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const batch = root.createBatch();
- const work = batch.render('Hi');
- let ops = [];
- work.then(() => {
- ops.push('inside callback: ' + container.textContent);
- });
- ops.push('before committing: ' + container.textContent);
- batch.commit();
- ops.push('after committing: ' + container.textContent);
- expect(ops).toEqual([
- 'before committing: ',
- // `then` callback should fire during commit phase
- 'inside callback: Hi',
- 'after committing: Hi',
- ]);
- });
-
- it('can commit an empty batch', () => {
- const root = ReactDOM.unstable_createRoot(container);
- root.render(1);
-
- Scheduler.unstable_advanceTime(2000);
- // This batch has a later expiration time than the earlier update.
- const batch = root.createBatch();
-
- // This should not flush the earlier update.
- batch.commit();
- expect(container.textContent).toEqual('');
-
- Scheduler.unstable_flushAll();
- expect(container.textContent).toEqual('1');
- });
-
- it('two batches created simultaneously are committed separately', () => {
- // (In other words, they have distinct expiration times)
- const root = ReactDOM.unstable_createRoot(container);
- const batch1 = root.createBatch();
- batch1.render(1);
- const batch2 = root.createBatch();
- batch2.render(2);
-
- expect(container.textContent).toEqual('');
-
- batch1.commit();
- expect(container.textContent).toEqual('1');
-
- batch2.commit();
- expect(container.textContent).toEqual('2');
- });
-
- it('commits an earlier batch without committing a later batch', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const batch1 = root.createBatch();
- batch1.render(1);
-
- // This batch has a later expiration time
- Scheduler.unstable_advanceTime(2000);
- const batch2 = root.createBatch();
- batch2.render(2);
-
- expect(container.textContent).toEqual('');
-
- batch1.commit();
- expect(container.textContent).toEqual('1');
-
- batch2.commit();
- expect(container.textContent).toEqual('2');
- });
-
- it('commits a later batch without committing an earlier batch', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const batch1 = root.createBatch();
- batch1.render(1);
-
- // This batch has a later expiration time
- Scheduler.unstable_advanceTime(2000);
- const batch2 = root.createBatch();
- batch2.render(2);
-
- expect(container.textContent).toEqual('');
-
- batch2.commit();
- expect(container.textContent).toEqual('2');
-
- batch1.commit();
- Scheduler.unstable_flushAll();
- expect(container.textContent).toEqual('1');
- });
-
- it('handles fatal errors triggered by batch.commit()', () => {
- const root = ReactDOM.unstable_createRoot(container);
- const batch = root.createBatch();
- const InvalidType = undefined;
- expect(() => batch.render()).toWarnDev(
- ['React.createElement: type is invalid'],
- {withoutStack: true},
- );
- expect(() => batch.commit()).toThrow('Element type is invalid');
- });
-
it('throws a good message on invalid containers', () => {
expect(() => {
ReactDOM.unstable_createRoot(Hi
);
diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js
index 753601dbecdd2..37f67e0ce8534 100644
--- a/packages/react-dom/src/client/ReactDOM.js
+++ b/packages/react-dom/src/client/ReactDOM.js
@@ -11,19 +11,13 @@ import type {ReactNodeList} from 'shared/ReactTypes';
import type {RootTag} from 'shared/ReactRootTags';
// TODO: This type is shared between the reconciler and ReactDOM, but will
// eventually be lifted out to the renderer.
-import type {
- FiberRoot,
- Batch as FiberRootBatch,
-} from 'react-reconciler/src/ReactFiberRoot';
+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
import '../shared/checkReact';
import './ReactDOMClientInjection';
import {
- computeUniqueAsyncExpiration,
findHostInstanceWithNoPortals,
- updateContainerAtExpirationTime,
- flushRoot,
createContainer,
updateContainer,
batchedEventUpdates,
@@ -179,209 +173,21 @@ setRestoreImplementation(restoreControlledState);
export type DOMContainer =
| (Element & {
- _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
+ _reactRootContainer: ?_ReactRoot,
_reactHasBeenPassedToCreateRootDEV: ?boolean,
})
| (Document & {
- _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
+ _reactRootContainer: ?_ReactRoot,
_reactHasBeenPassedToCreateRootDEV: ?boolean,
});
-type Batch = FiberRootBatch & {
- render(children: ReactNodeList): Work,
- then(onComplete: () => mixed): void,
- commit(): void,
-
- // The ReactRoot constructor is hoisted but the prototype methods are not. If
- // we move ReactRoot to be above ReactBatch, the inverse error occurs.
- // $FlowFixMe Hoisting issue.
- _root: _ReactRoot | _ReactSyncRoot,
- _hasChildren: boolean,
- _children: ReactNodeList,
-
- _callbacks: Array<() => mixed> | null,
- _didComplete: boolean,
-};
-
-type _ReactSyncRoot = {
- render(children: ReactNodeList, callback: ?() => mixed): Work,
- unmount(callback: ?() => mixed): Work,
+type _ReactRoot = {
+ render(children: ReactNodeList, callback: ?() => mixed): void,
+ unmount(callback: ?() => mixed): void,
_internalRoot: FiberRoot,
};
-type _ReactRoot = _ReactSyncRoot & {
- createBatch(): Batch,
-};
-
-function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
- const expirationTime = computeUniqueAsyncExpiration();
- this._expirationTime = expirationTime;
- this._root = root;
- this._next = null;
- this._callbacks = null;
- this._didComplete = false;
- this._hasChildren = false;
- this._children = null;
- this._defer = true;
-}
-ReactBatch.prototype.render = function(children: ReactNodeList) {
- invariant(
- this._defer,
- 'batch.render: Cannot render a batch that already committed.',
- );
- this._hasChildren = true;
- this._children = children;
- const internalRoot = this._root._internalRoot;
- const expirationTime = this._expirationTime;
- const work = new ReactWork();
- updateContainerAtExpirationTime(
- children,
- internalRoot,
- null,
- expirationTime,
- null,
- work._onCommit,
- );
- return work;
-};
-ReactBatch.prototype.then = function(onComplete: () => mixed) {
- if (this._didComplete) {
- onComplete();
- return;
- }
- let callbacks = this._callbacks;
- if (callbacks === null) {
- callbacks = this._callbacks = [];
- }
- callbacks.push(onComplete);
-};
-ReactBatch.prototype.commit = function() {
- const internalRoot = this._root._internalRoot;
- let firstBatch = internalRoot.firstBatch;
- invariant(
- this._defer && firstBatch !== null,
- 'batch.commit: Cannot commit a batch multiple times.',
- );
-
- if (!this._hasChildren) {
- // This batch is empty. Return.
- this._next = null;
- this._defer = false;
- return;
- }
-
- let expirationTime = this._expirationTime;
-
- // Ensure this is the first batch in the list.
- if (firstBatch !== this) {
- // This batch is not the earliest batch. We need to move it to the front.
- // Update its expiration time to be the expiration time of the earliest
- // batch, so that we can flush it without flushing the other batches.
- if (this._hasChildren) {
- expirationTime = this._expirationTime = firstBatch._expirationTime;
- // Rendering this batch again ensures its children will be the final state
- // when we flush (updates are processed in insertion order: last
- // update wins).
- // TODO: This forces a restart. Should we print a warning?
- this.render(this._children);
- }
-
- // Remove the batch from the list.
- let previous = null;
- let batch = firstBatch;
- while (batch !== this) {
- previous = batch;
- batch = batch._next;
- }
- invariant(
- previous !== null,
- 'batch.commit: Cannot commit a batch multiple times.',
- );
- previous._next = batch._next;
-
- // Add it to the front.
- this._next = firstBatch;
- firstBatch = internalRoot.firstBatch = this;
- }
-
- // Synchronously flush all the work up to this batch's expiration time.
- this._defer = false;
- flushRoot(internalRoot, expirationTime);
-
- // Pop the batch from the list.
- const next = this._next;
- this._next = null;
- firstBatch = internalRoot.firstBatch = next;
-
- // Append the next earliest batch's children to the update queue.
- if (firstBatch !== null && firstBatch._hasChildren) {
- firstBatch.render(firstBatch._children);
- }
-};
-ReactBatch.prototype._onComplete = function() {
- if (this._didComplete) {
- return;
- }
- this._didComplete = true;
- const callbacks = this._callbacks;
- if (callbacks === null) {
- return;
- }
- // TODO: Error handling.
- for (let i = 0; i < callbacks.length; i++) {
- const callback = callbacks[i];
- callback();
- }
-};
-
-type Work = {
- then(onCommit: () => mixed): void,
- _onCommit: () => void,
- _callbacks: Array<() => mixed> | null,
- _didCommit: boolean,
-};
-
-function ReactWork() {
- this._callbacks = null;
- this._didCommit = false;
- // TODO: Avoid need to bind by replacing callbacks in the update queue with
- // list of Work objects.
- this._onCommit = this._onCommit.bind(this);
-}
-ReactWork.prototype.then = function(onCommit: () => mixed): void {
- if (this._didCommit) {
- onCommit();
- return;
- }
- let callbacks = this._callbacks;
- if (callbacks === null) {
- callbacks = this._callbacks = [];
- }
- callbacks.push(onCommit);
-};
-ReactWork.prototype._onCommit = function(): void {
- if (this._didCommit) {
- return;
- }
- this._didCommit = true;
- const callbacks = this._callbacks;
- if (callbacks === null) {
- return;
- }
- // TODO: Error handling.
- for (let i = 0; i < callbacks.length; i++) {
- const callback = callbacks[i];
- invariant(
- typeof callback === 'function',
- 'Invalid argument passed as callback. Expected a function. Instead ' +
- 'received: %s',
- callback,
- );
- callback();
- }
-};
-
function createRootImpl(
container: DOMContainer,
tag: RootTag,
@@ -418,64 +224,24 @@ function ReactRoot(container: DOMContainer, options: void | RootOptions) {
ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
-): Work {
+): void {
const root = this._internalRoot;
- const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
- if (callback !== null) {
- work.then(callback);
- }
- updateContainer(children, root, null, work._onCommit);
- return work;
+ updateContainer(children, root, null, callback);
};
ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
callback: ?() => mixed,
-): Work {
+): void {
const root = this._internalRoot;
- const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
- if (callback !== null) {
- work.then(callback);
- }
- updateContainer(null, root, null, work._onCommit);
- return work;
-};
-
-// Sync roots cannot create batches. Only concurrent ones.
-ReactRoot.prototype.createBatch = function(): Batch {
- const batch = new ReactBatch(this);
- const expirationTime = batch._expirationTime;
-
- const internalRoot = this._internalRoot;
- const firstBatch = internalRoot.firstBatch;
- if (firstBatch === null) {
- internalRoot.firstBatch = batch;
- batch._next = null;
- } else {
- // Insert sorted by expiration time then insertion order
- let insertAfter = null;
- let insertBefore = firstBatch;
- while (
- insertBefore !== null &&
- insertBefore._expirationTime >= expirationTime
- ) {
- insertAfter = insertBefore;
- insertBefore = insertBefore._next;
- }
- batch._next = insertBefore;
- if (insertAfter !== null) {
- insertAfter._next = batch;
- }
- }
-
- return batch;
+ updateContainer(null, root, null, callback);
};
/**
@@ -529,7 +295,7 @@ let warnedAboutHydrateAPI = false;
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
-): _ReactSyncRoot {
+): _ReactRoot {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
@@ -593,7 +359,7 @@ function legacyRenderSubtreeIntoContainer(
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
- let root: _ReactSyncRoot = (container._reactRootContainer: any);
+ let root: _ReactRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// Initial mount
@@ -899,7 +665,7 @@ function createRoot(
function createSyncRoot(
container: DOMContainer,
options?: RootOptions,
-): _ReactSyncRoot {
+): _ReactRoot {
const functionName = enableStableConcurrentModeAPIs
? 'createRoot'
: 'unstable_createRoot';
diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
index 65e01fff98c24..bb442d81d9d12 100644
--- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
@@ -857,7 +857,7 @@ describe('DOMEventResponderSystem', () => {
function Test({counter}) {
const listener = React.unstable_useResponder(TestResponder, {counter});
-
+ Scheduler.unstable_yieldValue('Test');
return (