@samitouri / QOS-React-2 / commits / 862f499fac

Add Batched Mode (#15502)

* Add Batched Mode React has an unfortunate quirk where updates are sometimes synchronous -- where React starts rendering immediately within the call stack of `setState` — and sometimes batched, where updates are flushed at the end of the current event. Any update that originates within the call stack of the React event system is batched. This encompasses most updates, since most updates originate from an event handler like `onClick` or `onChange`. It also includes updates triggered by lifecycle methods or effects. But there are also updates that originate outside React's event system, like timer events, network events, and microtasks (promise resolution handlers). These are not batched, which results in both worse performance (multiple render passes instead of single one) and confusing semantics. Ideally all updates would be batched by default. Unfortunately, it's easy for components to accidentally rely on this behavior, so changing it could break existing apps in subtle ways. One way to move to a batched-by-default model is to opt into Concurrent Mode (still experimental). But Concurrent Mode introduces additional semantic changes that apps may not be ready to adopt. This commit introduces an additional mode called Batched Mode. Batched Mode enables a batched-by-default model that defers all updates to the next React event. Once it begins rendering, React will not yield to the browser until the entire render is finished. Batched Mode is superset of Strict Mode. It fires all the same warnings. It also drops the forked Suspense behavior used by Legacy Mode, in favor of the proper semantics used by Concurrent Mode. I have not added any public APIs that expose the new mode yet. I'll do that in subsequent commits. * Suspense in Batched Mode Should have same semantics as Concurrent Mode. * Use RootTag field to configure type of root There are three types of roots: Legacy, Batched, and Concurrent. * flushSync should not flush batched work Treat Sync and Batched expiration times separately. Only Sync updates are pushed to our internal queue of synchronous callbacks. Renamed `flushImmediateQueue` to `flushSyncCallbackQueue` for clarity.

Andrew Clark committed May 13, 2019 at 14:30 UTC 862f499facfba9635f21c25b17368cb980b17c7e
24 files changed +498 -238
packages/react-art/src/ReactART.js
+2 -1
@@ -7,6 +7,7 @@
7
8 import React from 'react';
9 import ReactVersion from 'shared/ReactVersion';
10 +import {LegacyRoot} from 'shared/ReactRootTags';
11 import {
12 createContainer,
13 updateContainer,
@@ -65,7 +66,7 @@ class Surface extends React.Component {
66
67 this._surface = Mode.Surface(+width, +height, this._tagRef);
68
68 - this._mountNode = createContainer(this._surface);
69 + this._mountNode = createContainer(this._surface, LegacyRoot, false);
70 updateContainer(this.props.children, this._mountNode, this);
71 }
72
packages/react-dom/src/client/ReactDOM.js
+6 -10
@@ -8,6 +8,7 @@
8 */
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 +import type {RootTag} from 'shared/ReactRootTags';
12 // TODO: This type is shared between the reconciler and ReactDOM, but will
13 // eventually be lifted out to the renderer.
14 import type {
@@ -52,6 +53,7 @@ import {
53 accumulateTwoPhaseDispatches,
54 accumulateDirectDispatches,
55 } from 'events/EventPropagators';
56 +import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
57 import {has as hasInstance} from 'shared/ReactInstanceMap';
58 import ReactVersion from 'shared/ReactVersion';
59 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -361,12 +363,8 @@ ReactWork.prototype._onCommit = function(): void {
363 }
364 };
365
364 -function ReactRoot(
365 - container: DOMContainer,
366 - isConcurrent: boolean,
367 - hydrate: boolean,
368 -) {
369 - const root = createContainer(container, isConcurrent, hydrate);
366 +function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
367 + const root = createContainer(container, tag, hydrate);
368 this._internalRoot = root;
369 }
370 ReactRoot.prototype.render = function(
@@ -531,9 +529,7 @@ function legacyCreateRootFromDOMContainer(
529 );
530 }
531 }
534 - // Legacy roots are not async by default.
535 - const isConcurrent = false;
536 - return new ReactRoot(container, isConcurrent, shouldHydrate);
532 + return new ReactRoot(container, LegacyRoot, shouldHydrate);
533 }
534
535 function legacyRenderSubtreeIntoContainer(
@@ -849,7 +845,7 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
845 container._reactHasBeenPassedToCreateRootDEV = true;
846 }
847 const hydrate = options != null && options.hydrate === true;
852 - return new ReactRoot(container, true, hydrate);
848 + return new ReactRoot(container, ConcurrentRoot, hydrate);
849 }
850
851 if (enableStableConcurrentModeAPIs) {
packages/react-dom/src/fire/ReactFire.js
+6 -10
@@ -13,6 +13,7 @@
13 // console.log('Hello from Fire entry point.');
14
15 import type {ReactNodeList} from 'shared/ReactTypes';
16 +import type {RootTag} from 'shared/ReactRootTags';
17 // TODO: This type is shared between the reconciler and ReactDOM, but will
18 // eventually be lifted out to the renderer.
19 import type {
@@ -58,6 +59,7 @@ import {
59 accumulateTwoPhaseDispatches,
60 accumulateDirectDispatches,
61 } from 'events/EventPropagators';
62 +import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
63 import {has as hasInstance} from 'shared/ReactInstanceMap';
64 import ReactVersion from 'shared/ReactVersion';
65 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -367,12 +369,8 @@ ReactWork.prototype._onCommit = function(): void {
369 }
370 };
371
370 -function ReactRoot(
371 - container: DOMContainer,
372 - isConcurrent: boolean,
373 - hydrate: boolean,
374 -) {
375 - const root = createContainer(container, isConcurrent, hydrate);
372 +function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
373 + const root = createContainer(container, tag, hydrate);
374 this._internalRoot = root;
375 }
376 ReactRoot.prototype.render = function(
@@ -537,9 +535,7 @@ function legacyCreateRootFromDOMContainer(
535 );
536 }
537 }
540 - // Legacy roots are not async by default.
541 - const isConcurrent = false;
542 - return new ReactRoot(container, isConcurrent, shouldHydrate);
538 + return new ReactRoot(container, LegacyRoot, shouldHydrate);
539 }
540
541 function legacyRenderSubtreeIntoContainer(
@@ -855,7 +851,7 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
851 container._reactHasBeenPassedToCreateRootDEV = true;
852 }
853 const hydrate = options != null && options.hydrate === true;
858 - return new ReactRoot(container, true, hydrate);
854 + return new ReactRoot(container, ConcurrentRoot, hydrate);
855 }
856
857 if (enableStableConcurrentModeAPIs) {
packages/react-native-renderer/src/ReactFabric.js
+2 -1
@@ -33,6 +33,7 @@ import ReactNativeComponent from './ReactNativeComponent';
33 import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
34 import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';
35
36 +import {LegacyRoot} from 'shared/ReactRootTags';
37 import ReactSharedInternals from 'shared/ReactSharedInternals';
38 import getComponentName from 'shared/getComponentName';
39 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -119,7 +120,7 @@ const ReactFabric: ReactFabricType = {
120 if (!root) {
121 // TODO (bvaughn): If we decide to keep the wrapper component,
122 // We could create a wrapper for containerTag as well to reduce special casing.
122 - root = createContainer(containerTag, false, false);
123 + root = createContainer(containerTag, LegacyRoot, false);
124 roots.set(containerTag, root);
125 }
126 updateContainer(element, root, null, callback);
packages/react-native-renderer/src/ReactNativeRenderer.js
+2 -1
@@ -40,6 +40,7 @@ import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
40 import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';
41 import {setNativeProps} from './ReactNativeRendererSharedExports';
42
43 +import {LegacyRoot} from 'shared/ReactRootTags';
44 import ReactSharedInternals from 'shared/ReactSharedInternals';
45 import getComponentName from 'shared/getComponentName';
46 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -125,7 +126,7 @@ const ReactNativeRenderer: ReactNativeType = {
126 if (!root) {
127 // TODO (bvaughn): If we decide to keep the wrapper component,
128 // We could create a wrapper for containerTag as well to reduce special casing.
128 - root = createContainer(containerTag, false, false);
129 + root = createContainer(containerTag, LegacyRoot, false);
130 roots.set(containerTag, root);
131 }
132 updateContainer(element, root, null, callback);
packages/react-noop-renderer/src/createReactNoop.js
+119 -54
@@ -18,6 +18,7 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
18 import type {Fiber} from 'react-reconciler/src/ReactFiber';
19 import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
20 import type {ReactNodeList} from 'shared/ReactTypes';
21 +import type {RootTag} from 'shared/ReactRootTags';
22
23 import * as Scheduler from 'scheduler/unstable_mock';
24 import {createPortal} from 'shared/ReactPortal';
@@ -32,6 +33,7 @@ import enqueueTask from 'shared/enqueueTask';
33 import ReactSharedInternals from 'shared/ReactSharedInternals';
34 import warningWithoutStack from 'shared/warningWithoutStack';
35 import {enableEventAPI} from 'shared/ReactFeatureFlags';
36 +import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags';
37
38 type EventTargetChildElement = {
39 type: string,
@@ -832,77 +834,145 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
834 return textInstance.text;
835 }
836
837 + function getChildren(root) {
838 + if (root) {
839 + return root.children;
840 + } else {
841 + return null;
842 + }
843 + }
844 +
845 + function getPendingChildren(root) {
846 + if (root) {
847 + return root.pendingChildren;
848 + } else {
849 + return null;
850 + }
851 + }
852 +
853 + function getChildrenAsJSX(root) {
854 + const children = childToJSX(getChildren(root), null);
855 + if (children === null) {
856 + return null;
857 + }
858 + if (Array.isArray(children)) {
859 + return {
860 + $$typeof: REACT_ELEMENT_TYPE,
861 + type: REACT_FRAGMENT_TYPE,
862 + key: null,
863 + ref: null,
864 + props: {children},
865 + _owner: null,
866 + _store: __DEV__ ? {} : undefined,
867 + };
868 + }
869 + return children;
870 + }
871 +
872 + function getPendingChildrenAsJSX(root) {
873 + const children = childToJSX(getChildren(root), null);
874 + if (children === null) {
875 + return null;
876 + }
877 + if (Array.isArray(children)) {
878 + return {
879 + $$typeof: REACT_ELEMENT_TYPE,
880 + type: REACT_FRAGMENT_TYPE,
881 + key: null,
882 + ref: null,
883 + props: {children},
884 + _owner: null,
885 + _store: __DEV__ ? {} : undefined,
886 + };
887 + }
888 + return children;
889 + }
890 +
891 + let idCounter = 0;
892 +
893 const ReactNoop = {
894 _Scheduler: Scheduler,
895
896 getChildren(rootID: string = DEFAULT_ROOT_ID) {
897 const container = rootContainers.get(rootID);
840 - if (container) {
841 - return container.children;
842 - } else {
843 - return null;
844 - }
898 + return getChildren(container);
899 },
900
901 getPendingChildren(rootID: string = DEFAULT_ROOT_ID) {
902 const container = rootContainers.get(rootID);
849 - if (container) {
850 - return container.pendingChildren;
851 - } else {
852 - return null;
853 - }
903 + return getPendingChildren(container);
904 },
905
856 - getOrCreateRootContainer(
857 - rootID: string = DEFAULT_ROOT_ID,
858 - isConcurrent: boolean = false,
859 - ) {
906 + getOrCreateRootContainer(rootID: string = DEFAULT_ROOT_ID, tag: RootTag) {
907 let root = roots.get(rootID);
908 if (!root) {
909 const container = {rootID: rootID, pendingChildren: [], children: []};
910 rootContainers.set(rootID, container);
864 - root = NoopRenderer.createContainer(container, isConcurrent, false);
911 + root = NoopRenderer.createContainer(container, tag, false);
912 roots.set(rootID, root);
913 }
914 return root.current.stateNode.containerInfo;
915 },
916
917 + // TODO: Replace ReactNoop.render with createRoot + root.render
918 + createRoot() {
919 + const container = {
920 + rootID: '' + idCounter++,
921 + pendingChildren: [],
922 + children: [],
923 + };
924 + const fiberRoot = NoopRenderer.createContainer(
925 + container,
926 + ConcurrentRoot,
927 + false,
928 + );
929 + return {
930 + _Scheduler: Scheduler,
931 + render(children: ReactNodeList) {
932 + NoopRenderer.updateContainer(children, fiberRoot, null, null);
933 + },
934 + getChildren() {
935 + return getChildren(fiberRoot);
936 + },
937 + getChildrenAsJSX() {
938 + return getChildrenAsJSX(fiberRoot);
939 + },
940 + };
941 + },
942 +
943 + createSyncRoot() {
944 + const container = {
945 + rootID: '' + idCounter++,
946 + pendingChildren: [],
947 + children: [],
948 + };
949 + const fiberRoot = NoopRenderer.createContainer(
950 + container,
951 + BatchedRoot,
952 + false,
953 + );
954 + return {
955 + _Scheduler: Scheduler,
956 + render(children: ReactNodeList) {
957 + NoopRenderer.updateContainer(children, fiberRoot, null, null);
958 + },
959 + getChildren() {
960 + return getChildren(container);
961 + },
962 + getChildrenAsJSX() {
963 + return getChildrenAsJSX(container);
964 + },
965 + };
966 + },
967 +
968 getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
871 - const children = childToJSX(ReactNoop.getChildren(rootID), null);
872 - if (children === null) {
873 - return null;
874 - }
875 - if (Array.isArray(children)) {
876 - return {
877 - $$typeof: REACT_ELEMENT_TYPE,
878 - type: REACT_FRAGMENT_TYPE,
879 - key: null,
880 - ref: null,
881 - props: {children},
882 - _owner: null,
883 - _store: __DEV__ ? {} : undefined,
884 - };
885 - }
886 - return children;
969 + const container = rootContainers.get(rootID);
970 + return getChildrenAsJSX(container);
971 },
972
973 getPendingChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
890 - const children = childToJSX(ReactNoop.getPendingChildren(rootID), null);
891 - if (children === null) {
892 - return null;
893 - }
894 - if (Array.isArray(children)) {
895 - return {
896 - $$typeof: REACT_ELEMENT_TYPE,
897 - type: REACT_FRAGMENT_TYPE,
898 - key: null,
899 - ref: null,
900 - props: {children},
901 - _owner: null,
902 - _store: __DEV__ ? {} : undefined,
903 - };
904 - }
905 - return children;
974 + const container = rootContainers.get(rootID);
975 + return getPendingChildrenAsJSX(container);
976 },
977
978 createPortal(
@@ -920,11 +990,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
990
991 renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) {
992 const rootID = DEFAULT_ROOT_ID;
923 - const isConcurrent = false;
924 - const container = ReactNoop.getOrCreateRootContainer(
925 - rootID,
926 - isConcurrent,
927 - );
993 + const container = ReactNoop.getOrCreateRootContainer(rootID, LegacyRoot);
994 const root = roots.get(container.rootID);
995 NoopRenderer.updateContainer(element, root, null, callback);
996 },
@@ -934,10 +1000,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1000 rootID: string,
1001 callback: ?Function,
1002 ) {
937 - const isConcurrent = true;
1003 const container = ReactNoop.getOrCreateRootContainer(
1004 rootID,
940 - isConcurrent,
1005 + ConcurrentRoot,
1006 );
1007 const root = roots.get(container.rootID);
1008 NoopRenderer.updateContainer(element, root, null, callback);
packages/react-reconciler/src/ReactFiber.js
+21 -37
@@ -15,6 +15,7 @@ import type {
15 ReactEventComponent,
16 ReactEventTarget,
17 } from 'shared/ReactTypes';
18 +import type {RootTag} from 'shared/ReactRootTags';
19 import type {WorkTag} from 'shared/ReactWorkTags';
20 import type {TypeOfMode} from './ReactTypeOfMode';
21 import type {SideEffectTag} from 'shared/ReactSideEffectTags';
@@ -27,6 +28,7 @@ import invariant from 'shared/invariant';
28 import warningWithoutStack from 'shared/warningWithoutStack';
29 import {enableProfilerTimer, enableEventAPI} from 'shared/ReactFeatureFlags';
30 import {NoEffect} from 'shared/ReactSideEffectTags';
31 +import {ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
32 import {
33 IndeterminateComponent,
34 ClassComponent,
@@ -52,10 +54,11 @@ import getComponentName from 'shared/getComponentName';
54 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
55 import {NoWork} from './ReactFiberExpirationTime';
56 import {
55 - NoContext,
57 + NoMode,
58 ConcurrentMode,
59 ProfileMode,
60 StrictMode,
61 + BatchedMode,
62 } from './ReactTypeOfMode';
63 import {
64 REACT_FORWARD_REF_TYPE,
@@ -434,8 +437,15 @@ export function createWorkInProgress(
437 return workInProgress;
438 }
439
437 -export function createHostRootFiber(isConcurrent: boolean): Fiber {
438 - let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext;
440 +export function createHostRootFiber(tag: RootTag): Fiber {
441 + let mode;
442 + if (tag === ConcurrentRoot) {
443 + mode = ConcurrentMode | BatchedMode | StrictMode;
444 + } else if (tag === BatchedRoot) {
445 + mode = BatchedMode | StrictMode;
446 + } else {
447 + mode = NoMode;
448 + }
449
450 if (enableProfilerTimer && isDevToolsPresent) {
451 // Always collect profile timings when DevTools are present.
@@ -476,19 +486,13 @@ export function createFiberFromTypeAndProps(
486 key,
487 );
488 case REACT_CONCURRENT_MODE_TYPE:
479 - return createFiberFromMode(
480 - pendingProps,
481 - mode | ConcurrentMode | StrictMode,
482 - expirationTime,
483 - key,
484 - );
489 + fiberTag = Mode;
490 + mode |= ConcurrentMode | BatchedMode | StrictMode;
491 + break;
492 case REACT_STRICT_MODE_TYPE:
486 - return createFiberFromMode(
487 - pendingProps,
488 - mode | StrictMode,
489 - expirationTime,
490 - key,
491 - );
493 + fiberTag = Mode;
494 + mode |= StrictMode;
495 + break;
496 case REACT_PROFILER_TYPE:
497 return createFiberFromProfiler(pendingProps, mode, expirationTime, key);
498 case REACT_SUSPENSE_TYPE:
@@ -672,26 +676,6 @@ function createFiberFromProfiler(
676 return fiber;
677 }
678
675 -function createFiberFromMode(
676 - pendingProps: any,
677 - mode: TypeOfMode,
678 - expirationTime: ExpirationTime,
679 - key: null | string,
680 -): Fiber {
681 - const fiber = createFiber(Mode, pendingProps, key, mode);
682 -
683 - // TODO: The Mode fiber shouldn't have a type. It has a tag.
684 - const type =
685 - (mode & ConcurrentMode) === NoContext
686 - ? REACT_STRICT_MODE_TYPE
687 - : REACT_CONCURRENT_MODE_TYPE;
688 - fiber.elementType = type;
689 - fiber.type = type;
690 -
691 - fiber.expirationTime = expirationTime;
692 - return fiber;
693 -}
694 -
679 export function createFiberFromSuspense(
680 pendingProps: any,
681 mode: TypeOfMode,
@@ -720,7 +704,7 @@ export function createFiberFromText(
704 }
705
706 export function createFiberFromHostInstanceForDeletion(): Fiber {
723 - const fiber = createFiber(HostComponent, null, null, NoContext);
707 + const fiber = createFiber(HostComponent, null, null, NoMode);
708 // TODO: These should not need a type.
709 fiber.elementType = 'DELETED';
710 fiber.type = 'DELETED';
@@ -751,7 +735,7 @@ export function assignFiberPropertiesInDEV(
735 if (target === null) {
736 // This Fiber's initial properties will always be overwritten.
737 // We only use a Fiber to ensure the same hidden class so DEV isn't slow.
754 - target = createFiber(IndeterminateComponent, null, null, NoContext);
738 + target = createFiber(IndeterminateComponent, null, null, NoMode);
739 }
740
741 // This is intentionally written as a list of all properties.
packages/react-reconciler/src/ReactFiberBeginWork.js
+8 -7
@@ -85,9 +85,10 @@ import {
85 } from './ReactFiberExpirationTime';
86 import {
87 ConcurrentMode,
88 - NoContext,
88 + NoMode,
89 ProfileMode,
90 StrictMode,
91 + BatchedMode,
92 } from './ReactTypeOfMode';
93 import {
94 shouldSetTextContent,
@@ -1535,8 +1536,8 @@ function updateSuspenseComponent(
1536 null,
1537 );
1538
1538 - if ((workInProgress.mode & ConcurrentMode) === NoContext) {
1539 - // Outside of concurrent mode, we commit the effects from the
1539 + if ((workInProgress.mode & BatchedMode) === NoMode) {
1540 + // Outside of batched mode, we commit the effects from the
1541 // partially completed, timed-out tree, too.
1542 const progressedState: SuspenseState = workInProgress.memoizedState;
1543 const progressedPrimaryChild: Fiber | null =
@@ -1588,8 +1589,8 @@ function updateSuspenseComponent(
1589 NoWork,
1590 );
1591
1591 - if ((workInProgress.mode & ConcurrentMode) === NoContext) {
1592 - // Outside of concurrent mode, we commit the effects from the
1592 + if ((workInProgress.mode & BatchedMode) === NoMode) {
1593 + // Outside of batched mode, we commit the effects from the
1594 // partially completed, timed-out tree, too.
1595 const progressedState: SuspenseState = workInProgress.memoizedState;
1596 const progressedPrimaryChild: Fiber | null =
@@ -1671,8 +1672,8 @@ function updateSuspenseComponent(
1672 // schedule a placement.
1673 // primaryChildFragment.effectTag |= Placement;
1674
1674 - if ((workInProgress.mode & ConcurrentMode) === NoContext) {
1675 - // Outside of concurrent mode, we commit the effects from the
1675 + if ((workInProgress.mode & BatchedMode) === NoMode) {
1676 + // Outside of batched mode, we commit the effects from the
1677 // partially completed, timed-out tree, too.
1678 const progressedState: SuspenseState = workInProgress.memoizedState;
1679 const progressedPrimaryChild: Fiber | null =
packages/react-reconciler/src/ReactFiberCompleteWork.js
+3 -3
@@ -43,7 +43,7 @@ import {
43 EventComponent,
44 EventTarget,
45 } from 'shared/ReactWorkTags';
46 -import {ConcurrentMode, NoContext} from './ReactTypeOfMode';
46 +import {NoMode, BatchedMode} from './ReactTypeOfMode';
47 import {
48 Placement,
49 Ref,
@@ -718,12 +718,12 @@ function completeWork(
718 }
719
720 if (nextDidTimeout && !prevDidTimeout) {
721 - // If this subtreee is running in concurrent mode we can suspend,
721 + // If this subtreee is running in batched mode we can suspend,
722 // otherwise we won't suspend.
723 // TODO: This will still suspend a synchronous tree if anything
724 // in the concurrent tree already suspended during this render.
725 // This is a known bug.
726 - if ((workInProgress.mode & ConcurrentMode) !== NoContext) {
726 + if ((workInProgress.mode & BatchedMode) !== NoMode) {
727 renderDidSuspend();
728 }
729 }
packages/react-reconciler/src/ReactFiberExpirationTime.js
+2 -1
@@ -23,9 +23,10 @@ export type ExpirationTime = number;
23 export const NoWork = 0;
24 export const Never = 1;
25 export const Sync = MAX_SIGNED_31_BIT_INT;
26 +export const Batched = Sync - 1;
27
28 const UNIT_SIZE = 10;
28 -const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;
29 +const MAGIC_NUMBER_OFFSET = Batched - 1;
30
31 // 1 unit of expiration time represents 10ms.
32 export function msToExpirationTime(ms: number): ExpirationTime {
packages/react-reconciler/src/ReactFiberReconciler.js
+3 -2
@@ -9,6 +9,7 @@
9
10 import type {Fiber} from './ReactFiber';
11 import type {FiberRoot} from './ReactFiberRoot';
12 +import type {RootTag} from 'shared/ReactRootTags';
13 import type {
14 Instance,
15 TextInstance,
@@ -273,10 +274,10 @@ function findHostInstanceWithWarning(
274
275 export function createContainer(
276 containerInfo: Container,
276 - isConcurrent: boolean,
277 + tag: RootTag,
278 hydrate: boolean,
279 ): OpaqueRoot {
279 - return createFiberRoot(containerInfo, isConcurrent, hydrate);
280 + return createFiberRoot(containerInfo, tag, hydrate);
281 }
282
283 export function updateContainer(
packages/react-reconciler/src/ReactFiberRoot.js
+9 -4
@@ -9,6 +9,7 @@
9
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 +import type {RootTag} from 'shared/ReactRootTags';
13 import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
14 import type {Thenable} from './ReactFiberScheduler';
15 import type {Interaction} from 'scheduler/src/Tracing';
@@ -30,6 +31,9 @@ export type Batch = {
31 export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
32
33 type BaseFiberRootProperties = {|
34 + // The type of root (legacy, batched, concurrent, etc.)
35 + tag: RootTag,
36 +
37 // Any additional information from the host associated with this root.
38 containerInfo: any,
39 // Used only by persistent updates.
@@ -89,7 +93,8 @@ export type FiberRoot = {
93 ...ProfilingOnlyFiberRootProperties,
94 };
95
92 -function FiberRootNode(containerInfo, hydrate) {
96 +function FiberRootNode(containerInfo, tag, hydrate) {
97 + this.tag = tag;
98 this.current = null;
99 this.containerInfo = containerInfo;
100 this.pendingChildren = null;
@@ -116,14 +121,14 @@ function FiberRootNode(containerInfo, hydrate) {
121
122 export function createFiberRoot(
123 containerInfo: any,
119 - isConcurrent: boolean,
124 + tag: RootTag,
125 hydrate: boolean,
126 ): FiberRoot {
122 - const root: FiberRoot = (new FiberRootNode(containerInfo, hydrate): any);
127 + const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
128
129 // Cyclic construction. This cheats the type system right now because
130 // stateNode is any.
126 - const uninitializedFiber = createHostRootFiber(isConcurrent);
131 + const uninitializedFiber = createHostRootFiber(tag);
132 root.current = uninitializedFiber;
133 uninitializedFiber.stateNode = root;
134
packages/react-reconciler/src/ReactFiberScheduler.js
+70 -56
@@ -41,7 +41,8 @@ import {
41 NormalPriority,
42 LowPriority,
43 IdlePriority,
44 - flushImmediateQueue,
44 + flushSyncCallbackQueue,
45 + scheduleSyncCallback,
46 } from './SchedulerWithReactIntegration';
47
48 import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
@@ -55,7 +56,12 @@ import {
56 } from './ReactFiberHostConfig';
57
58 import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
58 -import {NoContext, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
59 +import {
60 + NoMode,
61 + ProfileMode,
62 + BatchedMode,
63 + ConcurrentMode,
64 +} from './ReactTypeOfMode';
65 import {
66 HostRoot,
67 ClassComponent,
@@ -91,6 +97,7 @@ import {
97 computeAsyncExpiration,
98 inferPriorityFromExpirationTime,
99 LOW_PRIORITY_EXPIRATION,
100 + Batched,
101 } from './ReactFiberExpirationTime';
102 import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
103 import {completeWork} from './ReactFiberCompleteWork';
@@ -255,10 +262,16 @@ export function computeExpirationForFiber(
262 currentTime: ExpirationTime,
263 fiber: Fiber,
264 ): ExpirationTime {
258 - if ((fiber.mode & ConcurrentMode) === NoContext) {
265 + const mode = fiber.mode;
266 + if ((mode & BatchedMode) === NoMode) {
267 return Sync;
268 }
269
270 + const priorityLevel = getCurrentPriorityLevel();
271 + if ((mode & ConcurrentMode) === NoMode) {
272 + return priorityLevel === ImmediatePriority ? Sync : Batched;
273 + }
274 +
275 if (workPhase === RenderPhase) {
276 // Use whatever time we're already rendering
277 return renderExpirationTime;
@@ -266,7 +279,6 @@ export function computeExpirationForFiber(
279
280 // Compute an expiration time based on the Scheduler priority.
281 let expirationTime;
269 - const priorityLevel = getCurrentPriorityLevel();
282 switch (priorityLevel) {
283 case ImmediatePriority:
284 expirationTime = Sync;
@@ -349,7 +361,7 @@ export function scheduleUpdateOnFiber(
361 // scheduleCallbackForFiber to preserve the ability to schedule a callback
362 // without immediately flushing it. We only do this for user-initated
363 // updates, to preserve historical behavior of sync mode.
352 - flushImmediateQueue();
364 + flushSyncCallbackQueue();
365 }
366 }
367 } else {
@@ -456,36 +468,47 @@ function scheduleCallbackForRoot(
468 }
469 root.callbackExpirationTime = expirationTime;
470
459 - let options = null;
460 - if (expirationTime !== Sync && expirationTime !== Never) {
461 - let timeout = expirationTimeToMs(expirationTime) - now();
462 - if (timeout > 5000) {
463 - // Sanity check. Should never take longer than 5 seconds.
464 - // TODO: Add internal warning?
465 - timeout = 5000;
471 + if (expirationTime === Sync) {
472 + // Sync React callbacks are scheduled on a special internal queue
473 + root.callbackNode = scheduleSyncCallback(
474 + runRootCallback.bind(
475 + null,
476 + root,
477 + renderRoot.bind(null, root, expirationTime),
478 + ),
479 + );
480 + } else {
481 + let options = null;
482 + if (expirationTime !== Sync && expirationTime !== Never) {
483 + let timeout = expirationTimeToMs(expirationTime) - now();
484 + if (timeout > 5000) {
485 + // Sanity check. Should never take longer than 5 seconds.
486 + // TODO: Add internal warning?
487 + timeout = 5000;
488 + }
489 + options = {timeout};
490 }
467 - options = {timeout};
468 - }
491
470 - root.callbackNode = scheduleCallback(
471 - priorityLevel,
472 - runRootCallback.bind(
473 - null,
474 - root,
475 - renderRoot.bind(null, root, expirationTime),
476 - ),
477 - options,
478 - );
479 - if (
480 - enableUserTimingAPI &&
481 - expirationTime !== Sync &&
482 - workPhase !== RenderPhase &&
483 - workPhase !== CommitPhase
484 - ) {
485 - // Scheduled an async callback, and we're not already working. Add an
486 - // entry to the flamegraph that shows we're waiting for a callback
487 - // to fire.
488 - startRequestCallbackTimer();
492 + root.callbackNode = scheduleCallback(
493 + priorityLevel,
494 + runRootCallback.bind(
495 + null,
496 + root,
497 + renderRoot.bind(null, root, expirationTime),
498 + ),
499 + options,
500 + );
501 + if (
502 + enableUserTimingAPI &&
503 + expirationTime !== Sync &&
504 + workPhase !== RenderPhase &&
505 + workPhase !== CommitPhase
506 + ) {
507 + // Scheduled an async callback, and we're not already working. Add an
508 + // entry to the flamegraph that shows we're waiting for a callback
509 + // to fire.
510 + startRequestCallbackTimer();
511 + }
512 }
513 }
514
@@ -524,11 +547,8 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
547 'means you attempted to commit from inside a lifecycle method.',
548 );
549 }
527 - scheduleCallback(
528 - ImmediatePriority,
529 - renderRoot.bind(null, root, expirationTime),
530 - );
531 - flushImmediateQueue();
550 + scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
551 + flushSyncCallbackQueue();
552 }
553
554 export function flushInteractiveUpdates() {
@@ -596,13 +616,10 @@ function flushPendingDiscreteUpdates() {
616 const roots = rootsWithPendingDiscreteUpdates;
617 rootsWithPendingDiscreteUpdates = null;
618 roots.forEach((expirationTime, root) => {
599 - scheduleCallback(
600 - ImmediatePriority,
601 - renderRoot.bind(null, root, expirationTime),
602 - );
619 + scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
620 });
621 // Now flush the immediate queue.
605 - flushImmediateQueue();
622 + flushSyncCallbackQueue();
623 }
624 }
625
@@ -617,7 +634,7 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
634 } finally {
635 workPhase = NotWorking;
636 // Flush the immediate callbacks that were scheduled during this batch
620 - flushImmediateQueue();
637 + flushSyncCallbackQueue();
638 }
639 }
640
@@ -653,7 +670,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
670 // Flush the immediate callbacks that were scheduled during this batch.
671 // Note that this will happen even if batchedUpdates is higher up
672 // the stack.
656 - flushImmediateQueue();
673 + flushSyncCallbackQueue();
674 }
675 }
676
@@ -666,7 +683,7 @@ export function flushControlled(fn: () => mixed): void {
683 workPhase = prevWorkPhase;
684 if (workPhase === NotWorking) {
685 // Flush the immediate callbacks that were scheduled during this batch
669 - flushImmediateQueue();
686 + flushSyncCallbackQueue();
687 }
688 }
689 }
@@ -883,10 +900,7 @@ function renderRoot(
900 // caused by tearing due to a mutation during an event. Try rendering
901 // one more time without yiedling to events.
902 prepareFreshStack(root, expirationTime);
886 - scheduleCallback(
887 - ImmediatePriority,
888 - renderRoot.bind(null, root, expirationTime),
889 - );
903 + scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
904 return null;
905 }
906 // If we're already rendering synchronously, commit the root in its
@@ -990,7 +1004,7 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
1004 setCurrentDebugFiberInDEV(unitOfWork);
1005
1006 let next;
993 - if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoContext) {
1007 + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
1008 startProfilerTimer(unitOfWork);
1009 next = beginWork(current, unitOfWork, renderExpirationTime);
1010 stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
@@ -1026,7 +1040,7 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
1040 let next;
1041 if (
1042 !enableProfilerTimer ||
1029 - (workInProgress.mode & ProfileMode) === NoContext
1043 + (workInProgress.mode & ProfileMode) === NoMode
1044 ) {
1045 next = completeWork(current, workInProgress, renderExpirationTime);
1046 } else {
@@ -1092,7 +1106,7 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
1106
1107 if (
1108 enableProfilerTimer &&
1095 - (workInProgress.mode & ProfileMode) !== NoContext
1109 + (workInProgress.mode & ProfileMode) !== NoMode
1110 ) {
1111 // Record the render duration for the fiber that errored.
1112 stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
@@ -1156,7 +1170,7 @@ function resetChildExpirationTime(completedWork: Fiber) {
1170 let newChildExpirationTime = NoWork;
1171
1172 // Bubble up the earliest expiration time.
1159 - if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoContext) {
1173 + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
1174 // In profiling mode, resetChildExpirationTime is also used to reset
1175 // profiler durations.
1176 let actualDuration = completedWork.actualDuration;
@@ -1487,7 +1501,7 @@ function commitRootImpl(root, expirationTime) {
1501 }
1502
1503 // If layout work was scheduled, flush it now.
1490 - flushImmediateQueue();
1504 + flushSyncCallbackQueue();
1505 return null;
1506 }
1507
@@ -1658,7 +1672,7 @@ export function flushPassiveEffects() {
1672 }
1673
1674 workPhase = prevWorkPhase;
1661 - flushImmediateQueue();
1675 + flushSyncCallbackQueue();
1676
1677 // If additional passive effects were scheduled, increment a counter. If this
1678 // exceeds the limit, we'll fire a warning.
packages/react-reconciler/src/ReactFiberUnwindWork.js
+4 -4
@@ -42,7 +42,7 @@ import {
42 enableSuspenseServerRenderer,
43 enableEventAPI,
44 } from 'shared/ReactFeatureFlags';
45 -import {ConcurrentMode, NoContext} from './ReactTypeOfMode';
45 +import {NoMode, BatchedMode} from './ReactTypeOfMode';
46 import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent';
47
48 import {createCapturedValue} from './ReactCapturedValue';
@@ -238,15 +238,15 @@ function throwException(
238 thenables.add(thenable);
239 }
240
241 - // If the boundary is outside of concurrent mode, we should *not*
241 + // If the boundary is outside of batched mode, we should *not*
242 // suspend the commit. Pretend as if the suspended component rendered
243 // null and keep rendering. In the commit phase, we'll schedule a
244 // subsequent synchronous update to re-render the Suspense.
245 //
246 // Note: It doesn't matter whether the component that suspended was
247 - // inside a concurrent mode tree. If the Suspense is outside of it, we
247 + // inside a batched mode tree. If the Suspense is outside of it, we
248 // should *not* suspend the commit.
249 - if ((workInProgress.mode & ConcurrentMode) === NoContext) {
249 + if ((workInProgress.mode & BatchedMode) === NoMode) {
250 workInProgress.effectTag |= DidCapture;
251
252 // We're going to commit this fiber even though it didn't complete.
packages/react-reconciler/src/ReactTypeOfMode.js
+7 -4
@@ -9,7 +9,10 @@
9
10 export type TypeOfMode = number;
11
12 -export const NoContext = 0b000;
13 -export const ConcurrentMode = 0b001;
14 -export const StrictMode = 0b010;
15 -export const ProfileMode = 0b100;
12 +export const NoMode = 0b0000;
13 +export const StrictMode = 0b0001;
14 +// TODO: Remove BatchedMode and ConcurrentMode by reading from the root
15 +// tag instead
16 +export const BatchedMode = 0b0010;
17 +export const ConcurrentMode = 0b0100;
18 +export const ProfileMode = 0b1000;
packages/react-reconciler/src/SchedulerWithReactIntegration.js
+32 -32
@@ -69,9 +69,9 @@ export const shouldYield = disableYielding
69 ? () => false // Never yield when `disableYielding` is on
70 : Scheduler_shouldYield;
71
72 -let immediateQueue: Array<SchedulerCallback> | null = null;
72 +let syncQueue: Array<SchedulerCallback> | null = null;
73 let immediateQueueCallbackNode: mixed | null = null;
74 -let isFlushingImmediate: boolean = false;
74 +let isFlushingSyncQueue: boolean = false;
75 let initialTimeMs: number = Scheduler_now();
76
77 // If the initial timestamp is reasonably small, use Scheduler's `now` directly.
@@ -131,68 +131,68 @@ export function scheduleCallback(
131 callback: SchedulerCallback,
132 options: SchedulerCallbackOptions | void | null,
133 ) {
134 - if (reactPriorityLevel === ImmediatePriority) {
135 - // Push this callback into an internal queue. We'll flush these either in
136 - // the next tick, or earlier if something calls `flushImmediateQueue`.
137 - if (immediateQueue === null) {
138 - immediateQueue = [callback];
139 - // Flush the queue in the next tick, at the earliest.
140 - immediateQueueCallbackNode = Scheduler_scheduleCallback(
141 - Scheduler_ImmediatePriority,
142 - flushImmediateQueueImpl,
143 - );
144 - } else {
145 - // Push onto existing queue. Don't need to schedule a callback because
146 - // we already scheduled one when we created the queue.
147 - immediateQueue.push(callback);
148 - }
149 - return fakeCallbackNode;
150 - }
151 - // Otherwise pass through to Scheduler.
134 const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
135 return Scheduler_scheduleCallback(priorityLevel, callback, options);
136 }
137
138 +export function scheduleSyncCallback(callback: SchedulerCallback) {
139 + // Push this callback into an internal queue. We'll flush these either in
140 + // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
141 + if (syncQueue === null) {
142 + syncQueue = [callback];
143 + // Flush the queue in the next tick, at the earliest.
144 + immediateQueueCallbackNode = Scheduler_scheduleCallback(
145 + Scheduler_ImmediatePriority,
146 + flushSyncCallbackQueueImpl,
147 + );
148 + } else {
149 + // Push onto existing queue. Don't need to schedule a callback because
150 + // we already scheduled one when we created the queue.
151 + syncQueue.push(callback);
152 + }
153 + return fakeCallbackNode;
154 +}
155 +
156 export function cancelCallback(callbackNode: mixed) {
157 if (callbackNode !== fakeCallbackNode) {
158 Scheduler_cancelCallback(callbackNode);
159 }
160 }
161
162 -export function flushImmediateQueue() {
162 +export function flushSyncCallbackQueue() {
163 if (immediateQueueCallbackNode !== null) {
164 Scheduler_cancelCallback(immediateQueueCallbackNode);
165 }
166 - flushImmediateQueueImpl();
166 + flushSyncCallbackQueueImpl();
167 }
168
169 -function flushImmediateQueueImpl() {
170 - if (!isFlushingImmediate && immediateQueue !== null) {
169 +function flushSyncCallbackQueueImpl() {
170 + if (!isFlushingSyncQueue && syncQueue !== null) {
171 // Prevent re-entrancy.
172 - isFlushingImmediate = true;
172 + isFlushingSyncQueue = true;
173 let i = 0;
174 try {
175 const isSync = true;
176 - for (; i < immediateQueue.length; i++) {
177 - let callback = immediateQueue[i];
176 + for (; i < syncQueue.length; i++) {
177 + let callback = syncQueue[i];
178 do {
179 callback = callback(isSync);
180 } while (callback !== null);
181 }
182 - immediateQueue = null;
182 + syncQueue = null;
183 } catch (error) {
184 // If something throws, leave the remaining callbacks on the queue.
185 - if (immediateQueue !== null) {
186 - immediateQueue = immediateQueue.slice(i + 1);
185 + if (syncQueue !== null) {
186 + syncQueue = syncQueue.slice(i + 1);
187 }
188 // Resume flushing in the next tick
189 Scheduler_scheduleCallback(
190 Scheduler_ImmediatePriority,
191 - flushImmediateQueue,
191 + flushSyncCallbackQueue,
192 );
193 throw error;
194 } finally {
195 - isFlushingImmediate = false;
195 + isFlushingSyncQueue = false;
196 }
197 }
198 }
packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js new
+166
@@ -0,0 +1,166 @@
1 +let React;
2 +let ReactFeatureFlags;
3 +let ReactNoop;
4 +let act;
5 +let Scheduler;
6 +let ReactCache;
7 +let Suspense;
8 +let TextResource;
9 +
10 +describe('ReactBatchedMode', () => {
11 + beforeEach(() => {
12 + jest.resetModules();
13 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
14 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
15 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
16 + React = require('react');
17 + ReactNoop = require('react-noop-renderer');
18 + act = ReactNoop.act;
19 + Scheduler = require('scheduler');
20 + ReactCache = require('react-cache');
21 + Suspense = React.Suspense;
22 +
23 + TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
24 + return new Promise((resolve, reject) =>
25 + setTimeout(() => {
26 + Scheduler.yieldValue(`Promise resolved [${text}]`);
27 + resolve(text);
28 + }, ms),
29 + );
30 + }, ([text, ms]) => text);
31 + });
32 +
33 + function Text(props) {
34 + Scheduler.yieldValue(props.text);
35 + return props.text;
36 + }
37 +
38 + function AsyncText(props) {
39 + const text = props.text;
40 + try {
41 + TextResource.read([props.text, props.ms]);
42 + Scheduler.yieldValue(text);
43 + return props.text;
44 + } catch (promise) {
45 + if (typeof promise.then === 'function') {
46 + Scheduler.yieldValue(`Suspend! [${text}]`);
47 + } else {
48 + Scheduler.yieldValue(`Error! [${text}]`);
49 + }
50 + throw promise;
51 + }
52 + }
53 +
54 + it('updates flush without yielding in the next event', () => {
55 + const root = ReactNoop.createSyncRoot();
56 +
57 + root.render(
58 + <React.Fragment>
59 + <Text text="A" />
60 + <Text text="B" />
61 + <Text text="C" />
62 + </React.Fragment>,
63 + );
64 +
65 + // Nothing should have rendered yet
66 + expect(root).toMatchRenderedOutput(null);
67 +
68 + // Everything should render immediately in the next event
69 + expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
70 + expect(root).toMatchRenderedOutput('ABC');
71 + });
72 +
73 + it('layout updates flush synchronously in same event', () => {
74 + const {useLayoutEffect} = React;
75 +
76 + function App() {
77 + useLayoutEffect(() => {
78 + Scheduler.yieldValue('Layout effect');
79 + });
80 + return <Text text="Hi" />;
81 + }
82 +
83 + const root = ReactNoop.createSyncRoot();
84 + root.render(<App />);
85 + expect(root).toMatchRenderedOutput(null);
86 +
87 + expect(Scheduler).toFlushExpired(['Hi', 'Layout effect']);
88 + expect(root).toMatchRenderedOutput('Hi');
89 + });
90 +
91 + it('uses proper Suspense semantics, not legacy ones', async () => {
92 + const root = ReactNoop.createSyncRoot();
93 + root.render(
94 + <Suspense fallback={<Text text="Loading..." />}>
95 + <span>
96 + <Text text="A" />
97 + </span>
98 + <span>
99 + <AsyncText text="B" />
100 + </span>
101 + <span>
102 + <Text text="C" />
103 + </span>
104 + </Suspense>,
105 + );
106 +
107 + expect(Scheduler).toFlushExpired(['A', 'Suspend! [B]', 'C', 'Loading...']);
108 + // In Legacy Mode, A and B would mount in a hidden primary tree. In Batched
109 + // and Concurrent Mode, nothing in the primary tree should mount. But the
110 + // fallback should mount immediately.
111 + expect(root).toMatchRenderedOutput('Loading...');
112 +
113 + await jest.advanceTimersByTime(1000);
114 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
115 + expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
116 + expect(root).toMatchRenderedOutput(
117 + <React.Fragment>
118 + <span>A</span>
119 + <span>B</span>
120 + <span>C</span>
121 + </React.Fragment>,
122 + );
123 + });
124 +
125 + it('flushSync does not flush batched work', () => {
126 + const {useState, forwardRef, useImperativeHandle} = React;
127 + const root = ReactNoop.createSyncRoot();
128 +
129 + const Foo = forwardRef(({label}, ref) => {
130 + const [step, setStep] = useState(0);
131 + useImperativeHandle(ref, () => ({setStep}));
132 + return <Text text={label + step} />;
133 + });
134 +
135 + const foo1 = React.createRef(null);
136 + const foo2 = React.createRef(null);
137 + root.render(
138 + <React.Fragment>
139 + <Foo label="A" ref={foo1} />
140 + <Foo label="B" ref={foo2} />
141 + </React.Fragment>,
142 + );
143 +
144 + // Mount
145 + expect(Scheduler).toFlushExpired(['A0', 'B0']);
146 + expect(root).toMatchRenderedOutput('A0B0');
147 +
148 + // Schedule a batched update to the first sibling
149 + act(() => foo1.current.setStep(1));
150 +
151 + // Before it flushes, update the second sibling inside flushSync
152 + act(() =>
153 + ReactNoop.flushSync(() => {
154 + foo2.current.setStep(1);
155 + }),
156 + );
157 +
158 + // Only the second update should have flushed synchronously
159 + expect(Scheduler).toHaveYielded(['B1']);
160 + expect(root).toMatchRenderedOutput('A0B1');
161 +
162 + // Now flush the first update
163 + expect(Scheduler).toFlushExpired(['A1']);
164 + expect(root).toMatchRenderedOutput('A1B1');
165 + });
166 +});
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js renamed
+12 -2
@@ -12,12 +12,14 @@
12
13 let React;
14 let ReactFiberReconciler;
15 +let ConcurrentRoot;
16
17 describe('ReactFiberHostContext', () => {
18 beforeEach(() => {
19 jest.resetModules();
20 React = require('react');
21 ReactFiberReconciler = require('react-reconciler');
22 + ConcurrentRoot = require('shared/ReactRootTags');
23 });
24
25 it('works with null host context', () => {
@@ -58,7 +60,11 @@ describe('ReactFiberHostContext', () => {
60 supportsMutation: true,
61 });
62
61 - const container = Renderer.createContainer(/* root: */ null);
63 + const container = Renderer.createContainer(
64 + /* root: */ null,
65 + ConcurrentRoot,
66 + false,
67 + );
68 Renderer.updateContainer(
69 <a>
70 <b />
@@ -106,7 +112,11 @@ describe('ReactFiberHostContext', () => {
112 supportsMutation: true,
113 });
114
109 - const container = Renderer.createContainer(rootContext);
115 + const container = Renderer.createContainer(
116 + rootContext,
117 + ConcurrentRoot,
118 + false,
119 + );
120 Renderer.updateContainer(
121 <a>
122 <b />
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+1 -1
@@ -941,7 +941,7 @@ describe('ReactHooksWithNoopRenderer', () => {
941 });
942
943 it(
944 - 'in sync mode, useEffect is deferred and updates finish synchronously ' +
944 + 'in legacy mode, useEffect is deferred and updates finish synchronously ' +
945 '(in a single batch)',
946 () => {
947 function Counter(props) {
packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
+2 -2
@@ -175,8 +175,8 @@ describe('ReactSuspenseFuzz', () => {
175 resetCache();
176 ReactNoop.renderLegacySyncRoot(children);
177 resolveAllTasks();
178 - const syncOutput = ReactNoop.getChildrenAsJSX();
179 - expect(syncOutput).toEqual(expectedOutput);
178 + const legacyOutput = ReactNoop.getChildrenAsJSX();
179 + expect(legacyOutput).toEqual(expectedOutput);
180 ReactNoop.renderLegacySyncRoot(null);
181
182 resetCache();
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+2 -2
@@ -303,7 +303,7 @@ describe('ReactSuspensePlaceholder', () => {
303 });
304
305 describe('when suspending during mount', () => {
306 - it('properly accounts for base durations when a suspended times out in a sync tree', () => {
306 + it('properly accounts for base durations when a suspended times out in a legacy tree', () => {
307 ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
308 expect(Scheduler).toHaveYielded([
309 'App',
@@ -373,7 +373,7 @@ describe('ReactSuspensePlaceholder', () => {
373 });
374
375 describe('when suspending during update', () => {
376 - it('properly accounts for base durations when a suspended times out in a sync tree', () => {
376 + it('properly accounts for base durations when a suspended times out in a legacy tree', () => {
377 ReactNoop.renderLegacySyncRoot(
378 <App shouldSuspend={false} textRenderDuration={5} />,
379 );
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+3 -3
@@ -873,7 +873,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
873 );
874 });
875
876 - describe('sync mode', () => {
876 + describe('legacy mode mode', () => {
877 it('times out immediately', async () => {
878 function App() {
879 return (
@@ -981,7 +981,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
981
982 it(
983 'continues rendering asynchronously even if a promise is captured by ' +
984 - 'a sync boundary (default mode)',
984 + 'a sync boundary (legacy mode)',
985 async () => {
986 class UpdatingText extends React.Component {
987 state = {text: this.props.initialText};
@@ -1113,7 +1113,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1113
1114 it(
1115 'continues rendering asynchronously even if a promise is captured by ' +
1116 - 'a sync boundary (strict, non-concurrent)',
1116 + 'a sync boundary (strict, legacy)',
1117 async () => {
1118 class UpdatingText extends React.Component {
1119 state = {text: this.props.initialText};
packages/react-test-renderer/src/ReactTestRenderer.js
+2 -1
@@ -43,6 +43,7 @@ import ReactVersion from 'shared/ReactVersion';
43 import act from './ReactTestRendererAct';
44
45 import {getPublicInstance} from './ReactTestHostConfig';
46 +import {ConcurrentRoot, LegacyRoot} from 'shared/ReactRootTags';
47
48 type TestRendererOptions = {
49 createNodeMock: (element: React$Element<any>) => any,
@@ -439,7 +440,7 @@ const ReactTestRendererFiber = {
440 };
441 let root: FiberRoot | null = createContainer(
442 container,
442 - isConcurrent,
443 + isConcurrent ? ConcurrentRoot : LegacyRoot,
444 false,
445 );
446 invariant(root != null, 'something went wrong');
packages/shared/ReactRootTags.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export type RootTag = 0 | 1 | 2;
11 +
12 +export const LegacyRoot = 0;
13 +export const BatchedRoot = 1;
14 +export const ConcurrentRoot = 2;