@samitouri / QOS-React-2 / commits / 283ce53204

Add ReactDOM.unstable_createSyncRoot (#15504)

* Add ReactDOM.unstable_createSyncRoot - `ReactDOM.unstable_createRoot` creates a Concurrent Mode root. - `ReactDOM.unstable_createSyncRoot` creates a Batched Mode root. It does not support `createBatch`. - `ReactDOM.render` creates a Legacy Mode root. It will eventually be deprecated and possibly moved to a separate entry point, like `react-dom/legacy`. * Rename internal ReactRoot types

Andrew Clark committed May 13, 2019 at 15:30 UTC 283ce532042c195d27996a556c04142ccf93f567
3 files changed +174 -119
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
+31
@@ -661,6 +661,37 @@ describe('ReactDOMFiberAsync', () => {
661 });
662 });
663
664 + describe('createSyncRoot', () => {
665 + it('updates flush without yielding in the next event', () => {
666 + const root = ReactDOM.unstable_createSyncRoot(container);
667 +
668 + function Text(props) {
669 + Scheduler.yieldValue(props.text);
670 + return props.text;
671 + }
672 +
673 + root.render(
674 + <React.Fragment>
675 + <Text text="A" />
676 + <Text text="B" />
677 + <Text text="C" />
678 + </React.Fragment>,
679 + );
680 +
681 + // Nothing should have rendered yet
682 + expect(container.textContent).toEqual('');
683 +
684 + // Everything should render immediately in the next event
685 + expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
686 + expect(container.textContent).toEqual('ABC');
687 + });
688 +
689 + it('does not support createBatch', () => {
690 + const root = ReactDOM.unstable_createSyncRoot(container);
691 + expect(root.createBatch).toBe(undefined);
692 + });
693 + });
694 +
695 describe('Disable yielding', () => {
696 beforeEach(() => {
697 jest.resetModules();
packages/react-dom/src/client/ReactDOM.js
+71 -59
@@ -53,7 +53,7 @@ import {
53 accumulateTwoPhaseDispatches,
54 accumulateDirectDispatches,
55 } from 'events/EventPropagators';
56 -import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
56 +import {LegacyRoot, ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
57 import {has as hasInstance} from 'shared/ReactInstanceMap';
58 import ReactVersion from 'shared/ReactVersion';
59 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -159,11 +159,11 @@ setRestoreImplementation(restoreControlledState);
159
160 export type DOMContainer =
161 | (Element & {
162 - _reactRootContainer: ?Root,
162 + _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
163 _reactHasBeenPassedToCreateRootDEV: ?boolean,
164 })
165 | (Document & {
166 - _reactRootContainer: ?Root,
166 + _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
167 _reactHasBeenPassedToCreateRootDEV: ?boolean,
168 });
169
@@ -175,7 +175,7 @@ type Batch = FiberRootBatch & {
175 // The ReactRoot constructor is hoisted but the prototype methods are not. If
176 // we move ReactRoot to be above ReactBatch, the inverse error occurs.
177 // $FlowFixMe Hoisting issue.
178 - _root: Root,
178 + _root: _ReactRoot | _ReactSyncRoot,
179 _hasChildren: boolean,
180 _children: ReactNodeList,
181
@@ -183,20 +183,18 @@ type Batch = FiberRootBatch & {
183 _didComplete: boolean,
184 };
185
186 -type Root = {
186 +type _ReactSyncRoot = {
187 render(children: ReactNodeList, callback: ?() => mixed): Work,
188 unmount(callback: ?() => mixed): Work,
189 - legacy_renderSubtreeIntoContainer(
190 - parentComponent: ?React$Component<any, any>,
191 - children: ReactNodeList,
192 - callback: ?() => mixed,
193 - ): Work,
194 - createBatch(): Batch,
189
190 _internalRoot: FiberRoot,
191 };
192
199 -function ReactBatch(root: ReactRoot) {
193 +type _ReactRoot = _ReactSyncRoot & {
194 + createBatch(): Batch,
195 +};
196 +
197 +function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
198 const expirationTime = computeUniqueAsyncExpiration();
199 this._expirationTime = expirationTime;
200 this._root = root;
@@ -363,11 +361,22 @@ ReactWork.prototype._onCommit = function(): void {
361 }
362 };
363
366 -function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
364 +function ReactSyncRoot(
365 + container: DOMContainer,
366 + tag: RootTag,
367 + hydrate: boolean,
368 +) {
369 + // Tag is either LegacyRoot or Concurrent Root
370 const root = createContainer(container, tag, hydrate);
371 this._internalRoot = root;
372 }
370 -ReactRoot.prototype.render = function(
373 +
374 +function ReactRoot(container: DOMContainer, hydrate: boolean) {
375 + const root = createContainer(container, ConcurrentRoot, hydrate);
376 + this._internalRoot = root;
377 +}
378 +
379 +ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
380 children: ReactNodeList,
381 callback: ?() => mixed,
382 ): Work {
@@ -383,22 +392,8 @@ ReactRoot.prototype.render = function(
392 updateContainer(children, root, null, work._onCommit);
393 return work;
394 };
386 -ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work {
387 - const root = this._internalRoot;
388 - const work = new ReactWork();
389 - callback = callback === undefined ? null : callback;
390 - if (__DEV__) {
391 - warnOnInvalidCallback(callback, 'render');
392 - }
393 - if (callback !== null) {
394 - work.then(callback);
395 - }
396 - updateContainer(null, root, null, work._onCommit);
397 - return work;
398 -};
399 -ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
400 - parentComponent: ?React$Component<any, any>,
401 - children: ReactNodeList,
395 +
396 +ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
397 callback: ?() => mixed,
398 ): Work {
399 const root = this._internalRoot;
@@ -410,9 +405,11 @@ ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
405 if (callback !== null) {
406 work.then(callback);
407 }
413 - updateContainer(children, root, parentComponent, work._onCommit);
408 + updateContainer(null, root, null, work._onCommit);
409 return work;
410 };
411 +
412 +// Sync roots cannot create batches. Only concurrent ones.
413 ReactRoot.prototype.createBatch = function(): Batch {
414 const batch = new ReactBatch(this);
415 const expirationTime = batch._expirationTime;
@@ -492,7 +489,7 @@ let warnedAboutHydrateAPI = false;
489 function legacyCreateRootFromDOMContainer(
490 container: DOMContainer,
491 forceHydrate: boolean,
495 -): Root {
492 +): _ReactSyncRoot {
493 const shouldHydrate =
494 forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
495 // First clear any existing content.
@@ -529,7 +526,9 @@ function legacyCreateRootFromDOMContainer(
526 );
527 }
528 }
532 - return new ReactRoot(container, LegacyRoot, shouldHydrate);
529 +
530 + // Legacy roots are not batched.
531 + return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
532 }
533
534 function legacyRenderSubtreeIntoContainer(
@@ -541,56 +540,44 @@ function legacyRenderSubtreeIntoContainer(
540 ) {
541 if (__DEV__) {
542 topLevelUpdateWarnings(container);
543 + warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
544 }
545
546 // TODO: Without `any` type, Flow says "Property cannot be accessed on any
547 // member of intersection type." Whyyyyyy.
548 - let root: Root = (container._reactRootContainer: any);
548 + let root: _ReactSyncRoot = (container._reactRootContainer: any);
549 + let fiberRoot;
550 if (!root) {
551 // Initial mount
552 root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
553 container,
554 forceHydrate,
555 );
556 + fiberRoot = root._internalRoot;
557 if (typeof callback === 'function') {
558 const originalCallback = callback;
559 callback = function() {
558 - const instance = getPublicRootInstance(root._internalRoot);
560 + const instance = getPublicRootInstance(fiberRoot);
561 originalCallback.call(instance);
562 };
563 }
564 // Initial mount should not be batched.
565 unbatchedUpdates(() => {
564 - if (parentComponent != null) {
565 - root.legacy_renderSubtreeIntoContainer(
566 - parentComponent,
567 - children,
568 - callback,
569 - );
570 - } else {
571 - root.render(children, callback);
572 - }
566 + updateContainer(children, fiberRoot, parentComponent, callback);
567 });
568 } else {
569 + fiberRoot = root._internalRoot;
570 if (typeof callback === 'function') {
571 const originalCallback = callback;
572 callback = function() {
578 - const instance = getPublicRootInstance(root._internalRoot);
573 + const instance = getPublicRootInstance(fiberRoot);
574 originalCallback.call(instance);
575 };
576 }
577 // Update
583 - if (parentComponent != null) {
584 - root.legacy_renderSubtreeIntoContainer(
585 - parentComponent,
586 - children,
587 - callback,
588 - );
589 - } else {
590 - root.render(children, callback);
591 - }
578 + updateContainer(children, fiberRoot, parentComponent, callback);
579 }
593 - return getPublicRootInstance(root._internalRoot);
580 + return getPublicRootInstance(fiberRoot);
581 }
582
583 function createPortal(
@@ -800,6 +787,7 @@ const ReactDOM: Object = {
787 flushSync: flushSync,
788
789 unstable_createRoot: createRoot,
790 + unstable_createSyncRoot: createSyncRoot,
791 unstable_flushControlled: flushControlled,
792
793 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
@@ -826,7 +814,10 @@ type RootOptions = {
814 hydrate?: boolean,
815 };
816
829 -function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
817 +function createRoot(
818 + container: DOMContainer,
819 + options?: RootOptions,
820 +): _ReactRoot {
821 const functionName = enableStableConcurrentModeAPIs
822 ? 'createRoot'
823 : 'unstable_createRoot';
@@ -835,6 +826,29 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
826 '%s(...): Target container is not a DOM element.',
827 functionName,
828 );
829 + warnIfReactDOMContainerInDEV(container);
830 + const hydrate = options != null && options.hydrate === true;
831 + return new ReactRoot(container, hydrate);
832 +}
833 +
834 +function createSyncRoot(
835 + container: DOMContainer,
836 + options?: RootOptions,
837 +): _ReactSyncRoot {
838 + const functionName = enableStableConcurrentModeAPIs
839 + ? 'createRoot'
840 + : 'unstable_createRoot';
841 + invariant(
842 + isValidContainer(container),
843 + '%s(...): Target container is not a DOM element.',
844 + functionName,
845 + );
846 + warnIfReactDOMContainerInDEV(container);
847 + const hydrate = options != null && options.hydrate === true;
848 + return new ReactSyncRoot(container, BatchedRoot, hydrate);
849 +}
850 +
851 +function warnIfReactDOMContainerInDEV(container) {
852 if (__DEV__) {
853 warningWithoutStack(
854 !container._reactRootContainer,
@@ -844,13 +858,11 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
858 );
859 container._reactHasBeenPassedToCreateRootDEV = true;
860 }
847 - const hydrate = options != null && options.hydrate === true;
848 - return new ReactRoot(container, ConcurrentRoot, hydrate);
861 }
862
863 if (enableStableConcurrentModeAPIs) {
864 ReactDOM.createRoot = createRoot;
853 - ReactDOM.unstable_createRoot = undefined;
865 + ReactDOM.createSyncRoot = createSyncRoot;
866 }
867
868 const foundDevTools = injectIntoDevTools({
packages/react-dom/src/fire/ReactFire.js
+72 -60
@@ -59,7 +59,7 @@ import {
59 accumulateTwoPhaseDispatches,
60 accumulateDirectDispatches,
61 } from 'events/EventPropagators';
62 -import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
62 +import {LegacyRoot, ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
63 import {has as hasInstance} from 'shared/ReactInstanceMap';
64 import ReactVersion from 'shared/ReactVersion';
65 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -165,11 +165,11 @@ setRestoreImplementation(restoreControlledState);
165
166 export type DOMContainer =
167 | (Element & {
168 - _reactRootContainer: ?Root,
168 + _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
169 _reactHasBeenPassedToCreateRootDEV: ?boolean,
170 })
171 | (Document & {
172 - _reactRootContainer: ?Root,
172 + _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
173 _reactHasBeenPassedToCreateRootDEV: ?boolean,
174 });
175
@@ -181,7 +181,7 @@ type Batch = FiberRootBatch & {
181 // The ReactRoot constructor is hoisted but the prototype methods are not. If
182 // we move ReactRoot to be above ReactBatch, the inverse error occurs.
183 // $FlowFixMe Hoisting issue.
184 - _root: Root,
184 + _root: _ReactRoot | _ReactSyncRoot,
185 _hasChildren: boolean,
186 _children: ReactNodeList,
187
@@ -189,20 +189,18 @@ type Batch = FiberRootBatch & {
189 _didComplete: boolean,
190 };
191
192 -type Root = {
192 +type _ReactSyncRoot = {
193 render(children: ReactNodeList, callback: ?() => mixed): Work,
194 unmount(callback: ?() => mixed): Work,
195 - legacy_renderSubtreeIntoContainer(
196 - parentComponent: ?React$Component<any, any>,
197 - children: ReactNodeList,
198 - callback: ?() => mixed,
199 - ): Work,
200 - createBatch(): Batch,
195
196 _internalRoot: FiberRoot,
197 };
198
205 -function ReactBatch(root: ReactRoot) {
199 +type _ReactRoot = _ReactSyncRoot & {
200 + createBatch(): Batch,
201 +};
202 +
203 +function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
204 const expirationTime = computeUniqueAsyncExpiration();
205 this._expirationTime = expirationTime;
206 this._root = root;
@@ -369,11 +367,22 @@ ReactWork.prototype._onCommit = function(): void {
367 }
368 };
369
372 -function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
370 +function ReactSyncRoot(
371 + container: DOMContainer,
372 + tag: RootTag,
373 + hydrate: boolean,
374 +) {
375 + // Tag is either LegacyRoot or Concurrent Root
376 const root = createContainer(container, tag, hydrate);
377 this._internalRoot = root;
378 }
376 -ReactRoot.prototype.render = function(
379 +
380 +function ReactRoot(container: DOMContainer, hydrate: boolean) {
381 + const root = createContainer(container, ConcurrentRoot, hydrate);
382 + this._internalRoot = root;
383 +}
384 +
385 +ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
386 children: ReactNodeList,
387 callback: ?() => mixed,
388 ): Work {
@@ -389,22 +398,8 @@ ReactRoot.prototype.render = function(
398 updateContainer(children, root, null, work._onCommit);
399 return work;
400 };
392 -ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work {
393 - const root = this._internalRoot;
394 - const work = new ReactWork();
395 - callback = callback === undefined ? null : callback;
396 - if (__DEV__) {
397 - warnOnInvalidCallback(callback, 'render');
398 - }
399 - if (callback !== null) {
400 - work.then(callback);
401 - }
402 - updateContainer(null, root, null, work._onCommit);
403 - return work;
404 -};
405 -ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
406 - parentComponent: ?React$Component<any, any>,
407 - children: ReactNodeList,
401 +
402 +ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
403 callback: ?() => mixed,
404 ): Work {
405 const root = this._internalRoot;
@@ -416,9 +411,11 @@ ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
411 if (callback !== null) {
412 work.then(callback);
413 }
419 - updateContainer(children, root, parentComponent, work._onCommit);
414 + updateContainer(null, root, null, work._onCommit);
415 return work;
416 };
417 +
418 +// Sync roots cannot create batches. Only concurrent ones.
419 ReactRoot.prototype.createBatch = function(): Batch {
420 const batch = new ReactBatch(this);
421 const expirationTime = batch._expirationTime;
@@ -498,7 +495,7 @@ let warnedAboutHydrateAPI = false;
495 function legacyCreateRootFromDOMContainer(
496 container: DOMContainer,
497 forceHydrate: boolean,
501 -): Root {
498 +): _ReactSyncRoot {
499 const shouldHydrate =
500 forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
501 // First clear any existing content.
@@ -535,7 +532,9 @@ function legacyCreateRootFromDOMContainer(
532 );
533 }
534 }
538 - return new ReactRoot(container, LegacyRoot, shouldHydrate);
535 +
536 + // Legacy roots are not batched.
537 + return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
538 }
539
540 function legacyRenderSubtreeIntoContainer(
@@ -547,56 +546,44 @@ function legacyRenderSubtreeIntoContainer(
546 ) {
547 if (__DEV__) {
548 topLevelUpdateWarnings(container);
549 + warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
550 }
551
552 // TODO: Without `any` type, Flow says "Property cannot be accessed on any
553 // member of intersection type." Whyyyyyy.
554 - let root: Root = (container._reactRootContainer: any);
554 + let root: _ReactSyncRoot = (container._reactRootContainer: any);
555 + let fiberRoot;
556 if (!root) {
557 // Initial mount
558 root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
559 container,
560 forceHydrate,
561 );
562 + fiberRoot = root._internalRoot;
563 if (typeof callback === 'function') {
564 const originalCallback = callback;
565 callback = function() {
564 - const instance = getPublicRootInstance(root._internalRoot);
566 + const instance = getPublicRootInstance(fiberRoot);
567 originalCallback.call(instance);
568 };
569 }
570 // Initial mount should not be batched.
571 unbatchedUpdates(() => {
570 - if (parentComponent != null) {
571 - root.legacy_renderSubtreeIntoContainer(
572 - parentComponent,
573 - children,
574 - callback,
575 - );
576 - } else {
577 - root.render(children, callback);
578 - }
572 + updateContainer(children, fiberRoot, parentComponent, callback);
573 });
574 } else {
575 + fiberRoot = root._internalRoot;
576 if (typeof callback === 'function') {
577 const originalCallback = callback;
578 callback = function() {
584 - const instance = getPublicRootInstance(root._internalRoot);
579 + const instance = getPublicRootInstance(fiberRoot);
580 originalCallback.call(instance);
581 };
582 }
583 // Update
589 - if (parentComponent != null) {
590 - root.legacy_renderSubtreeIntoContainer(
591 - parentComponent,
592 - children,
593 - callback,
594 - );
595 - } else {
596 - root.render(children, callback);
597 - }
584 + updateContainer(children, fiberRoot, parentComponent, callback);
585 }
599 - return getPublicRootInstance(root._internalRoot);
586 + return getPublicRootInstance(fiberRoot);
587 }
588
589 function createPortal(
@@ -806,11 +793,12 @@ const ReactDOM: Object = {
793 flushSync: flushSync,
794
795 unstable_createRoot: createRoot,
796 + unstable_createSyncRoot: createSyncRoot,
797 unstable_flushControlled: flushControlled,
798
799 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
800 // Keep in sync with ReactDOMUnstableNativeDependencies.js
813 - // and ReactTestUtils.js. This is an array for better minification.
801 + // ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
802 Events: [
803 getInstanceFromNode,
804 getNodeFromInstance,
@@ -832,7 +820,10 @@ type RootOptions = {
820 hydrate?: boolean,
821 };
822
835 -function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
823 +function createRoot(
824 + container: DOMContainer,
825 + options?: RootOptions,
826 +): _ReactRoot {
827 const functionName = enableStableConcurrentModeAPIs
828 ? 'createRoot'
829 : 'unstable_createRoot';
@@ -841,6 +832,29 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
832 '%s(...): Target container is not a DOM element.',
833 functionName,
834 );
835 + warnIfReactDOMContainerInDEV(container);
836 + const hydrate = options != null && options.hydrate === true;
837 + return new ReactRoot(container, hydrate);
838 +}
839 +
840 +function createSyncRoot(
841 + container: DOMContainer,
842 + options?: RootOptions,
843 +): _ReactSyncRoot {
844 + const functionName = enableStableConcurrentModeAPIs
845 + ? 'createRoot'
846 + : 'unstable_createRoot';
847 + invariant(
848 + isValidContainer(container),
849 + '%s(...): Target container is not a DOM element.',
850 + functionName,
851 + );
852 + warnIfReactDOMContainerInDEV(container);
853 + const hydrate = options != null && options.hydrate === true;
854 + return new ReactSyncRoot(container, BatchedRoot, hydrate);
855 +}
856 +
857 +function warnIfReactDOMContainerInDEV(container) {
858 if (__DEV__) {
859 warningWithoutStack(
860 !container._reactRootContainer,
@@ -850,13 +864,11 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
864 );
865 container._reactHasBeenPassedToCreateRootDEV = true;
866 }
853 - const hydrate = options != null && options.hydrate === true;
854 - return new ReactRoot(container, ConcurrentRoot, hydrate);
867 }
868
869 if (enableStableConcurrentModeAPIs) {
870 ReactDOM.createRoot = createRoot;
859 - ReactDOM.unstable_createRoot = undefined;
871 + ReactDOM.createSyncRoot = createSyncRoot;
872 }
873
874 const foundDevTools = injectIntoDevTools({