From 67e8030886d97fe6b10e81848442fcad5d03d540 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 24 Apr 2019 20:25:41 -0700 Subject: [PATCH] Add Batched Mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/react-dom/src/client/ReactDOM.js | 3 +- packages/react-dom/src/fire/ReactFire.js | 3 +- .../src/ReactNativeRenderer.js | 2 +- .../src/createReactNoop.js | 175 +++++++++++++----- packages/react-reconciler/src/ReactFiber.js | 59 +++--- .../src/ReactFiberBeginWork.js | 8 +- .../src/ReactFiberCompleteWork.js | 4 +- .../src/ReactFiberExpirationTime.js | 3 +- .../src/ReactFiberReconciler.js | 3 +- .../react-reconciler/src/ReactFiberRoot.js | 3 +- .../src/ReactFiberScheduler.js | 25 ++- .../src/ReactFiberUnwindWork.js | 4 +- .../react-reconciler/src/ReactTypeOfMode.js | 9 +- .../ReactBatchedMode.test-internal.js | 58 ++++++ .../src/ReactTestRenderer.js | 2 + 15 files changed, 255 insertions(+), 106 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactBatchedMode.test-internal.js 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, );