diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 728f775adf5e2..af726812f5326 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -366,7 +366,8 @@ function ReactRoot( isConcurrent: boolean, hydrate: boolean, ) { - const root = createContainer(container, isConcurrent, hydrate); + const isBatched = false; + const root = createContainer(container, isBatched, isConcurrent, hydrate); this._internalRoot = root; } ReactRoot.prototype.render = function( diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js index e4f555ebd627d..514fa5b44470b 100644 --- a/packages/react-dom/src/fire/ReactFire.js +++ b/packages/react-dom/src/fire/ReactFire.js @@ -372,7 +372,8 @@ function ReactRoot( isConcurrent: boolean, hydrate: boolean, ) { - const root = createContainer(container, isConcurrent, hydrate); + const isBatched = false; + const root = createContainer(container, isBatched, isConcurrent, hydrate); this._internalRoot = root; } ReactRoot.prototype.render = function( diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 6cfe575dcb3a9..9189a50e3e18d 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -125,7 +125,7 @@ const ReactNativeRenderer: ReactNativeType = { if (!root) { // TODO (bvaughn): If we decide to keep the wrapper component, // We could create a wrapper for containerTag as well to reduce special casing. - root = createContainer(containerTag, false, false); + root = createContainer(containerTag, false, false, false); roots.set(containerTag, root); } updateContainer(element, root, null, callback); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 3ac1c7021de5d..93310773bc05d 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -832,77 +832,160 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return textInstance.text; } + function getChildren(root) { + if (root) { + return root.children; + } else { + return null; + } + } + + function getPendingChildren(root) { + if (root) { + return root.pendingChildren; + } else { + return null; + } + } + + function getChildrenAsJSX(root) { + const children = childToJSX(getChildren(root), null); + if (children === null) { + return null; + } + if (Array.isArray(children)) { + return { + $$typeof: REACT_ELEMENT_TYPE, + type: REACT_FRAGMENT_TYPE, + key: null, + ref: null, + props: {children}, + _owner: null, + _store: __DEV__ ? {} : undefined, + }; + } + return children; + } + + function getPendingChildrenAsJSX(root) { + const children = childToJSX(getChildren(root), null); + if (children === null) { + return null; + } + if (Array.isArray(children)) { + return { + $$typeof: REACT_ELEMENT_TYPE, + type: REACT_FRAGMENT_TYPE, + key: null, + ref: null, + props: {children}, + _owner: null, + _store: __DEV__ ? {} : undefined, + }; + } + return children; + } + + let idCounter = 0; + const ReactNoop = { _Scheduler: Scheduler, getChildren(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); - if (container) { - return container.children; - } else { - return null; - } + return getChildren(container); }, getPendingChildren(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); - if (container) { - return container.pendingChildren; - } else { - return null; - } + return getPendingChildren(container); }, getOrCreateRootContainer( rootID: string = DEFAULT_ROOT_ID, - isConcurrent: boolean = false, + isBatched: boolean, + isConcurrent: boolean, ) { let root = roots.get(rootID); if (!root) { const container = {rootID: rootID, pendingChildren: [], children: []}; rootContainers.set(rootID, container); - root = NoopRenderer.createContainer(container, isConcurrent, false); + root = NoopRenderer.createContainer( + container, + isBatched, + isConcurrent, + false, + ); roots.set(rootID, root); } return root.current.stateNode.containerInfo; }, + // TODO: Replace ReactNoop.render with createRoot + root.render + createRoot() { + const isBatched = true; + const isConcurrent = true; + const container = { + rootID: '' + idCounter++, + pendingChildren: [], + children: [], + }; + const fiberRoot = NoopRenderer.createContainer( + container, + isBatched, + isConcurrent, + false, + ); + return { + _Scheduler: Scheduler, + render(children: ReactNodeList) { + NoopRenderer.updateContainer(children, fiberRoot, null, null); + }, + getChildren() { + return getChildren(fiberRoot); + }, + getChildrenAsJSX() { + return getChildrenAsJSX(fiberRoot); + }, + }; + }, + + createSyncRoot() { + const isBatched = true; + const isConcurrent = false; + const container = { + rootID: '' + idCounter++, + pendingChildren: [], + children: [], + }; + const fiberRoot = NoopRenderer.createContainer( + container, + isBatched, + isConcurrent, + false, + ); + return { + _Scheduler: Scheduler, + render(children: ReactNodeList) { + NoopRenderer.updateContainer(children, fiberRoot, null, null); + }, + getChildren() { + return getChildren(container); + }, + getChildrenAsJSX() { + return getChildrenAsJSX(container); + }, + }; + }, + getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) { - const children = childToJSX(ReactNoop.getChildren(rootID), null); - if (children === null) { - return null; - } - if (Array.isArray(children)) { - return { - $$typeof: REACT_ELEMENT_TYPE, - type: REACT_FRAGMENT_TYPE, - key: null, - ref: null, - props: {children}, - _owner: null, - _store: __DEV__ ? {} : undefined, - }; - } - return children; + const container = rootContainers.get(rootID); + return getChildrenAsJSX(container); }, getPendingChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) { - const children = childToJSX(ReactNoop.getPendingChildren(rootID), null); - if (children === null) { - return null; - } - if (Array.isArray(children)) { - return { - $$typeof: REACT_ELEMENT_TYPE, - type: REACT_FRAGMENT_TYPE, - key: null, - ref: null, - props: {children}, - _owner: null, - _store: __DEV__ ? {} : undefined, - }; - } - return children; + const container = rootContainers.get(rootID); + return getPendingChildrenAsJSX(container); }, createPortal( @@ -920,9 +1003,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { renderLegacySyncRoot(element: React$Element, callback: ?Function) { const rootID = DEFAULT_ROOT_ID; + const isBatched = false; const isConcurrent = false; const container = ReactNoop.getOrCreateRootContainer( rootID, + isBatched, isConcurrent, ); const root = roots.get(container.rootID); @@ -934,9 +1019,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { rootID: string, callback: ?Function, ) { + const isBatched = true; const isConcurrent = true; const container = ReactNoop.getOrCreateRootContainer( rootID, + isBatched, isConcurrent, ); const root = roots.get(container.rootID); diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 8ca24a16a670c..38fafa1cd1ddd 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -52,10 +52,11 @@ import getComponentName from 'shared/getComponentName'; import {isDevToolsPresent} from './ReactFiberDevToolsHook'; import {NoWork} from './ReactFiberExpirationTime'; import { - NoContext, + NoMode, ConcurrentMode, ProfileMode, StrictMode, + BatchedMode, } from './ReactTypeOfMode'; import { REACT_FORWARD_REF_TYPE, @@ -434,8 +435,18 @@ export function createWorkInProgress( return workInProgress; } -export function createHostRootFiber(isConcurrent: boolean): Fiber { - let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext; +export function createHostRootFiber( + isBatched: boolean, + isConcurrent: boolean, +): Fiber { + let mode; + if (isConcurrent) { + mode = ConcurrentMode | BatchedMode | StrictMode; + } else if (isBatched) { + mode = BatchedMode | StrictMode; + } else { + mode = NoMode; + } if (enableProfilerTimer && isDevToolsPresent) { // Always collect profile timings when DevTools are present. @@ -476,19 +487,13 @@ export function createFiberFromTypeAndProps( key, ); case REACT_CONCURRENT_MODE_TYPE: - return createFiberFromMode( - pendingProps, - mode | ConcurrentMode | StrictMode, - expirationTime, - key, - ); + fiberTag = Mode; + mode |= ConcurrentMode | BatchedMode | StrictMode; + break; case REACT_STRICT_MODE_TYPE: - return createFiberFromMode( - pendingProps, - mode | StrictMode, - expirationTime, - key, - ); + fiberTag = Mode; + mode |= StrictMode; + break; case REACT_PROFILER_TYPE: return createFiberFromProfiler(pendingProps, mode, expirationTime, key); case REACT_SUSPENSE_TYPE: @@ -672,26 +677,6 @@ function createFiberFromProfiler( return fiber; } -function createFiberFromMode( - pendingProps: any, - mode: TypeOfMode, - expirationTime: ExpirationTime, - key: null | string, -): Fiber { - const fiber = createFiber(Mode, pendingProps, key, mode); - - // TODO: The Mode fiber shouldn't have a type. It has a tag. - const type = - (mode & ConcurrentMode) === NoContext - ? REACT_STRICT_MODE_TYPE - : REACT_CONCURRENT_MODE_TYPE; - fiber.elementType = type; - fiber.type = type; - - fiber.expirationTime = expirationTime; - return fiber; -} - export function createFiberFromSuspense( pendingProps: any, mode: TypeOfMode, @@ -720,7 +705,7 @@ export function createFiberFromText( } export function createFiberFromHostInstanceForDeletion(): Fiber { - const fiber = createFiber(HostComponent, null, null, NoContext); + const fiber = createFiber(HostComponent, null, null, NoMode); // TODO: These should not need a type. fiber.elementType = 'DELETED'; fiber.type = 'DELETED'; @@ -751,7 +736,7 @@ export function assignFiberPropertiesInDEV( if (target === null) { // This Fiber's initial properties will always be overwritten. // We only use a Fiber to ensure the same hidden class so DEV isn't slow. - target = createFiber(IndeterminateComponent, null, null, NoContext); + target = createFiber(IndeterminateComponent, null, null, NoMode); } // This is intentionally written as a list of all properties. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8659e010a9847..94f5b0cbb0262 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -84,7 +84,7 @@ import { } from './ReactFiberExpirationTime'; import { ConcurrentMode, - NoContext, + NoMode, ProfileMode, StrictMode, } from './ReactTypeOfMode'; @@ -1493,7 +1493,7 @@ function updateSuspenseComponent( null, ); - if ((workInProgress.mode & ConcurrentMode) === NoContext) { + if ((workInProgress.mode & ConcurrentMode) === NoMode) { // Outside of concurrent mode, we commit the effects from the // partially completed, timed-out tree, too. const progressedState: SuspenseState = workInProgress.memoizedState; @@ -1546,7 +1546,7 @@ function updateSuspenseComponent( NoWork, ); - if ((workInProgress.mode & ConcurrentMode) === NoContext) { + if ((workInProgress.mode & ConcurrentMode) === NoMode) { // Outside of concurrent mode, we commit the effects from the // partially completed, timed-out tree, too. const progressedState: SuspenseState = workInProgress.memoizedState; @@ -1629,7 +1629,7 @@ function updateSuspenseComponent( // schedule a placement. // primaryChildFragment.effectTag |= Placement; - if ((workInProgress.mode & ConcurrentMode) === NoContext) { + if ((workInProgress.mode & ConcurrentMode) === NoMode) { // Outside of concurrent mode, we commit the effects from the // partially completed, timed-out tree, too. const progressedState: SuspenseState = workInProgress.memoizedState; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 9bf288fc63ee2..dbd0c7e1743f4 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -43,7 +43,7 @@ import { EventComponent, EventTarget, } from 'shared/ReactWorkTags'; -import {ConcurrentMode, NoContext} from './ReactTypeOfMode'; +import {ConcurrentMode, NoMode} from './ReactTypeOfMode'; import { Placement, Ref, @@ -721,7 +721,7 @@ function completeWork( // TODO: This will still suspend a synchronous tree if anything // in the concurrent tree already suspended during this render. // This is a known bug. - if ((workInProgress.mode & ConcurrentMode) !== NoContext) { + if ((workInProgress.mode & ConcurrentMode) !== NoMode) { renderDidSuspend(); } } diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index a8594a57a3fe6..9efc530f38cc3 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -23,9 +23,10 @@ export type ExpirationTime = number; export const NoWork = 0; export const Never = 1; export const Sync = MAX_SIGNED_31_BIT_INT; +export const Batched = Sync - 1; const UNIT_SIZE = 10; -const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1; +const MAGIC_NUMBER_OFFSET = Batched - 1; // 1 unit of expiration time represents 10ms. export function msToExpirationTime(ms: number): ExpirationTime { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 77fadd31e9b2e..0ac8890390635 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -273,10 +273,11 @@ function findHostInstanceWithWarning( export function createContainer( containerInfo: Container, + isBatched: boolean, isConcurrent: boolean, hydrate: boolean, ): OpaqueRoot { - return createFiberRoot(containerInfo, isConcurrent, hydrate); + return createFiberRoot(containerInfo, isBatched, isConcurrent, hydrate); } export function updateContainer( diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 38563ae2533bd..9329194459559 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -116,6 +116,7 @@ function FiberRootNode(containerInfo, hydrate) { export function createFiberRoot( containerInfo: any, + isBatched: boolean, isConcurrent: boolean, hydrate: boolean, ): FiberRoot { @@ -123,7 +124,7 @@ export function createFiberRoot( // Cyclic construction. This cheats the type system right now because // stateNode is any. - const uninitializedFiber = createHostRootFiber(isConcurrent); + const uninitializedFiber = createHostRootFiber(isBatched, isConcurrent); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 4ba3436ea93de..a5b807cde9f3c 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -55,7 +55,12 @@ import { } from './ReactFiberHostConfig'; import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber'; -import {NoContext, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; +import { + NoMode, + ProfileMode, + BatchedMode, + ConcurrentMode, +} from './ReactTypeOfMode'; import { HostRoot, ClassComponent, @@ -91,6 +96,7 @@ import { computeAsyncExpiration, inferPriorityFromExpirationTime, LOW_PRIORITY_EXPIRATION, + Batched, } from './ReactFiberExpirationTime'; import {beginWork as originalBeginWork} from './ReactFiberBeginWork'; import {completeWork} from './ReactFiberCompleteWork'; @@ -255,10 +261,16 @@ export function computeExpirationForFiber( currentTime: ExpirationTime, fiber: Fiber, ): ExpirationTime { - if ((fiber.mode & ConcurrentMode) === NoContext) { + const mode = fiber.mode; + if ((mode & BatchedMode) === NoMode) { return Sync; } + const priorityLevel = getCurrentPriorityLevel(); + if ((mode & ConcurrentMode) === NoMode) { + return priorityLevel === ImmediatePriority ? Sync : Batched; + } + if (workPhase === RenderPhase) { // Use whatever time we're already rendering return renderExpirationTime; @@ -266,7 +278,6 @@ export function computeExpirationForFiber( // Compute an expiration time based on the Scheduler priority. let expirationTime; - const priorityLevel = getCurrentPriorityLevel(); switch (priorityLevel) { case ImmediatePriority: expirationTime = Sync; @@ -983,7 +994,7 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { setCurrentDebugFiberInDEV(unitOfWork); let next; - if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoContext) { + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); next = beginWork(current, unitOfWork, renderExpirationTime); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); @@ -1019,7 +1030,7 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { let next; if ( !enableProfilerTimer || - (workInProgress.mode & ProfileMode) === NoContext + (workInProgress.mode & ProfileMode) === NoMode ) { next = completeWork(current, workInProgress, renderExpirationTime); } else { @@ -1085,7 +1096,7 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { if ( enableProfilerTimer && - (workInProgress.mode & ProfileMode) !== NoContext + (workInProgress.mode & ProfileMode) !== NoMode ) { // Record the render duration for the fiber that errored. stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); @@ -1149,7 +1160,7 @@ function resetChildExpirationTime(completedWork: Fiber) { let newChildExpirationTime = NoWork; // Bubble up the earliest expiration time. - if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoContext) { + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { // In profiling mode, resetChildExpirationTime is also used to reset // profiler durations. let actualDuration = completedWork.actualDuration; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index c6c8276733037..a39632acf08cb 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -41,7 +41,7 @@ import { enableSuspenseServerRenderer, enableEventAPI, } from 'shared/ReactFeatureFlags'; -import {ConcurrentMode, NoContext} from './ReactTypeOfMode'; +import {ConcurrentMode, NoMode} from './ReactTypeOfMode'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent'; import {createCapturedValue} from './ReactCapturedValue'; @@ -231,7 +231,7 @@ function throwException( // Note: It doesn't matter whether the component that suspended was // inside a concurrent mode tree. If the Suspense is outside of it, we // should *not* suspend the commit. - if ((workInProgress.mode & ConcurrentMode) === NoContext) { + if ((workInProgress.mode & ConcurrentMode) === NoMode) { workInProgress.effectTag |= DidCapture; // We're going to commit this fiber even though it didn't complete. diff --git a/packages/react-reconciler/src/ReactTypeOfMode.js b/packages/react-reconciler/src/ReactTypeOfMode.js index a727bec843425..a097830ae89e6 100644 --- a/packages/react-reconciler/src/ReactTypeOfMode.js +++ b/packages/react-reconciler/src/ReactTypeOfMode.js @@ -9,7 +9,8 @@ export type TypeOfMode = number; -export const NoContext = 0b000; -export const ConcurrentMode = 0b001; -export const StrictMode = 0b010; -export const ProfileMode = 0b100; +export const NoMode = 0b0000; +export const StrictMode = 0b0001; +export const BatchedMode = 0b0010; +export const ConcurrentMode = 0b0100; +export const ProfileMode = 0b1000; diff --git a/packages/react-reconciler/src/__tests__/ReactBatchedMode.test-internal.js b/packages/react-reconciler/src/__tests__/ReactBatchedMode.test-internal.js new file mode 100644 index 0000000000000..ad85a7a4bc980 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactBatchedMode.test-internal.js @@ -0,0 +1,58 @@ +let React; +let ReactFeatureFlags; +let ReactNoop; +let Scheduler; + +describe('ReactBatchedMode', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + }); + + function Text(props) { + Scheduler.yieldValue(props.text); + return props.text; + } + + it('updates flush without yielding in the next event', () => { + const root = ReactNoop.createSyncRoot(); + + root.render( + + + + + , + ); + + // Nothing should have rendered yet + expect(root).toMatchRenderedOutput(null); + + // Everything should render immediately in the next event + expect(Scheduler).toFlushExpired(['A', 'B', 'C']); + expect(root).toMatchRenderedOutput('ABC'); + }); + + it('layout updates flush synchronously in same event', () => { + const {useLayoutEffect} = React; + + function App() { + useLayoutEffect(() => { + Scheduler.yieldValue('Layout effect'); + }); + return ; + } + + const root = ReactNoop.createSyncRoot(); + root.render(); + expect(root).toMatchRenderedOutput(null); + + expect(Scheduler).toFlushExpired(['Hi', 'Layout effect']); + expect(root).toMatchRenderedOutput('Hi'); + }); +}); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 20553ba821912..caa7670169490 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -437,8 +437,10 @@ const ReactTestRendererFiber = { createNodeMock, tag: 'CONTAINER', }; + const isBatched = false; let root: FiberRoot | null = createContainer( container, + isBatched, isConcurrent, false, );