Skip to content

Commit 82237ee

Browse files
committed
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.
1 parent 3f058de commit 82237ee

15 files changed

+258
-106
lines changed

packages/react-dom/src/client/ReactDOM.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,8 @@ function ReactRoot(
366366
isConcurrent: boolean,
367367
hydrate: boolean,
368368
) {
369-
const root = createContainer(container, isConcurrent, hydrate);
369+
const isBatched = false;
370+
const root = createContainer(container, isBatched, isConcurrent, hydrate);
370371
this._internalRoot = root;
371372
}
372373
ReactRoot.prototype.render = function(

packages/react-dom/src/fire/ReactFire.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,8 @@ function ReactRoot(
372372
isConcurrent: boolean,
373373
hydrate: boolean,
374374
) {
375-
const root = createContainer(container, isConcurrent, hydrate);
375+
const isBatched = false;
376+
const root = createContainer(container, isBatched, isConcurrent, hydrate);
376377
this._internalRoot = root;
377378
}
378379
ReactRoot.prototype.render = function(

packages/react-native-renderer/src/ReactNativeRenderer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const ReactNativeRenderer: ReactNativeType = {
125125
if (!root) {
126126
// TODO (bvaughn): If we decide to keep the wrapper component,
127127
// We could create a wrapper for containerTag as well to reduce special casing.
128-
root = createContainer(containerTag, false, false);
128+
root = createContainer(containerTag, false, false, false);
129129
roots.set(containerTag, root);
130130
}
131131
updateContainer(element, root, null, callback);

packages/react-noop-renderer/src/createReactNoop.js

+131-44
Original file line numberDiff line numberDiff line change
@@ -832,77 +832,160 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
832832
return textInstance.text;
833833
}
834834

835+
function getChildren(root) {
836+
if (root) {
837+
return root.children;
838+
} else {
839+
return null;
840+
}
841+
}
842+
843+
function getPendingChildren(root) {
844+
if (root) {
845+
return root.pendingChildren;
846+
} else {
847+
return null;
848+
}
849+
}
850+
851+
function getChildrenAsJSX(root) {
852+
const children = childToJSX(getChildren(root), null);
853+
if (children === null) {
854+
return null;
855+
}
856+
if (Array.isArray(children)) {
857+
return {
858+
$$typeof: REACT_ELEMENT_TYPE,
859+
type: REACT_FRAGMENT_TYPE,
860+
key: null,
861+
ref: null,
862+
props: {children},
863+
_owner: null,
864+
_store: __DEV__ ? {} : undefined,
865+
};
866+
}
867+
return children;
868+
}
869+
870+
function getPendingChildrenAsJSX(root) {
871+
const children = childToJSX(getChildren(root), 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;
887+
}
888+
889+
let idCounter = 0;
890+
835891
const ReactNoop = {
836892
_Scheduler: Scheduler,
837893

838894
getChildren(rootID: string = DEFAULT_ROOT_ID) {
839895
const container = rootContainers.get(rootID);
840-
if (container) {
841-
return container.children;
842-
} else {
843-
return null;
844-
}
896+
return getChildren(container);
845897
},
846898

847899
getPendingChildren(rootID: string = DEFAULT_ROOT_ID) {
848900
const container = rootContainers.get(rootID);
849-
if (container) {
850-
return container.pendingChildren;
851-
} else {
852-
return null;
853-
}
901+
return getPendingChildren(container);
854902
},
855903

856904
getOrCreateRootContainer(
857905
rootID: string = DEFAULT_ROOT_ID,
858-
isConcurrent: boolean = false,
906+
isBatched: boolean,
907+
isConcurrent: boolean,
859908
) {
860909
let root = roots.get(rootID);
861910
if (!root) {
862911
const container = {rootID: rootID, pendingChildren: [], children: []};
863912
rootContainers.set(rootID, container);
864-
root = NoopRenderer.createContainer(container, isConcurrent, false);
913+
root = NoopRenderer.createContainer(
914+
container,
915+
isBatched,
916+
isConcurrent,
917+
false,
918+
);
865919
roots.set(rootID, root);
866920
}
867921
return root.current.stateNode.containerInfo;
868922
},
869923

924+
// TODO: Remplace ReactNoop.render with createRoot + root.render
925+
createRoot() {
926+
const isBatched = true;
927+
const isConcurrent = true;
928+
const container = {
929+
rootID: '' + idCounter++,
930+
pendingChildren: [],
931+
children: [],
932+
};
933+
const fiberRoot = NoopRenderer.createContainer(
934+
container,
935+
isBatched,
936+
isConcurrent,
937+
false,
938+
);
939+
return {
940+
_Scheduler: Scheduler,
941+
render(children: ReactNodeList) {
942+
NoopRenderer.updateContainer(children, fiberRoot, null, null);
943+
},
944+
getChildren() {
945+
return getChildren(fiberRoot);
946+
},
947+
getChildrenAsJSX() {
948+
return getChildrenAsJSX(fiberRoot);
949+
},
950+
};
951+
},
952+
953+
createSyncRoot() {
954+
const isBatched = true;
955+
const isConcurrent = false;
956+
const container = {
957+
rootID: '' + idCounter++,
958+
pendingChildren: [],
959+
children: [],
960+
};
961+
const fiberRoot = NoopRenderer.createContainer(
962+
container,
963+
isBatched,
964+
isConcurrent,
965+
false,
966+
);
967+
return {
968+
_Scheduler: Scheduler,
969+
render(children: ReactNodeList) {
970+
NoopRenderer.updateContainer(children, fiberRoot, null, null);
971+
},
972+
getChildren() {
973+
return getChildren(container);
974+
},
975+
getChildrenAsJSX() {
976+
return getChildrenAsJSX(container);
977+
},
978+
};
979+
},
980+
870981
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;
982+
const container = rootContainers.get(rootID);
983+
return getChildrenAsJSX(container);
887984
},
888985

889986
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;
987+
const container = rootContainers.get(rootID);
988+
return getPendingChildrenAsJSX(container);
906989
},
907990

908991
createPortal(
@@ -920,9 +1003,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
9201003

9211004
renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) {
9221005
const rootID = DEFAULT_ROOT_ID;
1006+
const isBatched = false;
9231007
const isConcurrent = false;
9241008
const container = ReactNoop.getOrCreateRootContainer(
9251009
rootID,
1010+
isBatched,
9261011
isConcurrent,
9271012
);
9281013
const root = roots.get(container.rootID);
@@ -934,9 +1019,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
9341019
rootID: string,
9351020
callback: ?Function,
9361021
) {
1022+
const isBatched = true;
9371023
const isConcurrent = true;
9381024
const container = ReactNoop.getOrCreateRootContainer(
9391025
rootID,
1026+
isBatched,
9401027
isConcurrent,
9411028
);
9421029
const root = roots.get(container.rootID);

packages/react-reconciler/src/ReactFiber.js

+22-37
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ import getComponentName from 'shared/getComponentName';
5252
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
5353
import {NoWork} from './ReactFiberExpirationTime';
5454
import {
55-
NoContext,
55+
NoMode,
5656
ConcurrentMode,
5757
ProfileMode,
5858
StrictMode,
59+
BatchedMode,
5960
} from './ReactTypeOfMode';
6061
import {
6162
REACT_FORWARD_REF_TYPE,
@@ -434,8 +435,18 @@ export function createWorkInProgress(
434435
return workInProgress;
435436
}
436437

437-
export function createHostRootFiber(isConcurrent: boolean): Fiber {
438-
let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext;
438+
export function createHostRootFiber(
439+
isBatched: boolean,
440+
isConcurrent: boolean,
441+
): Fiber {
442+
let mode;
443+
if (isConcurrent) {
444+
mode = ConcurrentMode | BatchedMode | StrictMode;
445+
} else if (isBatched) {
446+
mode = BatchedMode | StrictMode;
447+
} else {
448+
mode = NoMode;
449+
}
439450

440451
if (enableProfilerTimer && isDevToolsPresent) {
441452
// Always collect profile timings when DevTools are present.
@@ -476,19 +487,13 @@ export function createFiberFromTypeAndProps(
476487
key,
477488
);
478489
case REACT_CONCURRENT_MODE_TYPE:
479-
return createFiberFromMode(
480-
pendingProps,
481-
mode | ConcurrentMode | StrictMode,
482-
expirationTime,
483-
key,
484-
);
490+
fiberTag = Mode;
491+
mode |= ConcurrentMode | BatchedMode | StrictMode;
492+
break;
485493
case REACT_STRICT_MODE_TYPE:
486-
return createFiberFromMode(
487-
pendingProps,
488-
mode | StrictMode,
489-
expirationTime,
490-
key,
491-
);
494+
fiberTag = Mode;
495+
mode |= StrictMode;
496+
break;
492497
case REACT_PROFILER_TYPE:
493498
return createFiberFromProfiler(pendingProps, mode, expirationTime, key);
494499
case REACT_SUSPENSE_TYPE:
@@ -672,26 +677,6 @@ function createFiberFromProfiler(
672677
return fiber;
673678
}
674679

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-
695680
export function createFiberFromSuspense(
696681
pendingProps: any,
697682
mode: TypeOfMode,
@@ -720,7 +705,7 @@ export function createFiberFromText(
720705
}
721706

722707
export function createFiberFromHostInstanceForDeletion(): Fiber {
723-
const fiber = createFiber(HostComponent, null, null, NoContext);
708+
const fiber = createFiber(HostComponent, null, null, NoMode);
724709
// TODO: These should not need a type.
725710
fiber.elementType = 'DELETED';
726711
fiber.type = 'DELETED';
@@ -751,7 +736,7 @@ export function assignFiberPropertiesInDEV(
751736
if (target === null) {
752737
// This Fiber's initial properties will always be overwritten.
753738
// We only use a Fiber to ensure the same hidden class so DEV isn't slow.
754-
target = createFiber(IndeterminateComponent, null, null, NoContext);
739+
target = createFiber(IndeterminateComponent, null, null, NoMode);
755740
}
756741

757742
// This is intentionally written as a list of all properties.

packages/react-reconciler/src/ReactFiberBeginWork.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ import {
8484
} from './ReactFiberExpirationTime';
8585
import {
8686
ConcurrentMode,
87-
NoContext,
87+
NoMode,
8888
ProfileMode,
8989
StrictMode,
9090
} from './ReactTypeOfMode';
@@ -1493,7 +1493,7 @@ function updateSuspenseComponent(
14931493
null,
14941494
);
14951495

1496-
if ((workInProgress.mode & ConcurrentMode) === NoContext) {
1496+
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
14971497
// Outside of concurrent mode, we commit the effects from the
14981498
// partially completed, timed-out tree, too.
14991499
const progressedState: SuspenseState = workInProgress.memoizedState;
@@ -1546,7 +1546,7 @@ function updateSuspenseComponent(
15461546
NoWork,
15471547
);
15481548

1549-
if ((workInProgress.mode & ConcurrentMode) === NoContext) {
1549+
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
15501550
// Outside of concurrent mode, we commit the effects from the
15511551
// partially completed, timed-out tree, too.
15521552
const progressedState: SuspenseState = workInProgress.memoizedState;
@@ -1629,7 +1629,7 @@ function updateSuspenseComponent(
16291629
// schedule a placement.
16301630
// primaryChildFragment.effectTag |= Placement;
16311631

1632-
if ((workInProgress.mode & ConcurrentMode) === NoContext) {
1632+
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
16331633
// Outside of concurrent mode, we commit the effects from the
16341634
// partially completed, timed-out tree, too.
16351635
const progressedState: SuspenseState = workInProgress.memoizedState;

0 commit comments

Comments
 (0)