diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js
index 0b16c0808368b..ea9dd9aba257e 100644
--- a/packages/react-dom/index.classic.fb.js
+++ b/packages/react-dom/index.classic.fb.js
@@ -30,6 +30,8 @@ export {
unmountComponentAtNode,
createRoot,
createRoot as unstable_createRoot,
+ createBlockingRoot,
+ createBlockingRoot as unstable_createBlockingRoot,
unstable_flushControlled,
unstable_scheduleHydration,
unstable_runWithPriority,
diff --git a/packages/react-dom/index.experimental.js b/packages/react-dom/index.experimental.js
index e685ec6e8a1f8..9ed70f3959ec9 100644
--- a/packages/react-dom/index.experimental.js
+++ b/packages/react-dom/index.experimental.js
@@ -20,6 +20,7 @@ export {
unmountComponentAtNode,
// exposeConcurrentModeAPIs
createRoot as unstable_createRoot,
+ createBlockingRoot as unstable_createBlockingRoot,
unstable_flushControlled,
unstable_scheduleHydration,
// DO NOT USE: Temporarily exposing this to migrate off of Scheduler.runWithPriority.
diff --git a/packages/react-dom/index.js b/packages/react-dom/index.js
index 7adfaa4ad2185..59825272c3a89 100644
--- a/packages/react-dom/index.js
+++ b/packages/react-dom/index.js
@@ -21,6 +21,8 @@ export {
unmountComponentAtNode,
createRoot,
createRoot as unstable_createRoot,
+ createBlockingRoot,
+ createBlockingRoot as unstable_createBlockingRoot,
unstable_flushControlled,
unstable_scheduleHydration,
unstable_runWithPriority,
diff --git a/packages/react-dom/index.modern.fb.js b/packages/react-dom/index.modern.fb.js
index f91cc3cb89553..addcce97749c9 100644
--- a/packages/react-dom/index.modern.fb.js
+++ b/packages/react-dom/index.modern.fb.js
@@ -15,6 +15,8 @@ export {
version,
createRoot,
createRoot as unstable_createRoot,
+ createBlockingRoot,
+ createBlockingRoot as unstable_createBlockingRoot,
unstable_flushControlled,
unstable_scheduleHydration,
unstable_runWithPriority,
diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
index ec3f19950a993..ee609d7c2e0c5 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
@@ -593,6 +593,33 @@ describe('ReactDOMFiberAsync', () => {
expect(containerC.textContent).toEqual('Finished');
});
+ describe('createBlockingRoot', () => {
+ // @gate experimental
+ it('updates flush without yielding in the next event', () => {
+ const root = ReactDOM.unstable_createBlockingRoot(container);
+
+ function Text(props) {
+ Scheduler.unstable_yieldValue(props.text);
+ return props.text;
+ }
+
+ root.render(
+ <>
+
+
+
+ >,
+ );
+
+ // Nothing should have rendered yet
+ expect(container.textContent).toEqual('');
+
+ // Everything should render immediately in the next event
+ expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
+ expect(container.textContent).toEqual('ABC');
+ });
+ });
+
// @gate experimental
it('unmounted roots should never clear newer root content from a container', () => {
const ref = React.createRef();
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index ec13c315157f1..2ad221d5fbcb6 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -352,7 +352,7 @@ describe('ReactDOMServerPartialHydration', () => {
}).toErrorDev(
'Warning: Cannot hydrate Suspense in legacy mode. Switch from ' +
'ReactDOM.hydrate(element, container) to ' +
- 'ReactDOM.createRoot(container, { hydrate: true })' +
+ 'ReactDOM.createBlockingRoot(container, { hydrate: true })' +
'.render(element) or remove the Suspense components from the server ' +
'rendered components.' +
'\n in Suspense (at **)' +
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
index 7b28d572ccceb..6c48b36bd107d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
@@ -127,7 +127,7 @@ describe('ReactDOMServerSuspense', () => {
expect(divB.textContent).toBe('B');
act(() => {
- const root = ReactDOM.createRoot(parent, {hydrate: true});
+ const root = ReactDOM.createBlockingRoot(parent, {hydrate: true});
root.render(example);
});
diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
index 0496e26692dab..57807679e8eb2 100644
--- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
@@ -72,6 +72,33 @@ describe('ReactTestUtils.act()', () => {
runActTests('legacy mode', renderLegacy, unmountLegacy, rerenderLegacy);
+ // and then in blocking mode
+ if (__EXPERIMENTAL__) {
+ let blockingRoot = null;
+ const renderBatched = (el, dom) => {
+ blockingRoot = ReactDOM.unstable_createBlockingRoot(dom);
+ blockingRoot.render(el);
+ };
+
+ const unmountBatched = dom => {
+ if (blockingRoot !== null) {
+ blockingRoot.unmount();
+ blockingRoot = null;
+ }
+ };
+
+ const rerenderBatched = el => {
+ blockingRoot.render(el);
+ };
+
+ runActTests(
+ 'blocking mode',
+ renderBatched,
+ unmountBatched,
+ rerenderBatched,
+ );
+ }
+
describe('unacted effects', () => {
function App() {
React.useEffect(() => {}, []);
@@ -97,6 +124,19 @@ describe('ReactTestUtils.act()', () => {
]);
});
+ // @gate experimental
+ it('warns in blocking mode', () => {
+ expect(() => {
+ const root = ReactDOM.unstable_createBlockingRoot(
+ document.createElement('div'),
+ );
+ root.render();
+ Scheduler.unstable_flushAll();
+ }).toErrorDev([
+ 'An update to App ran an effect, but was not wrapped in act(...)',
+ ]);
+ });
+
// @gate experimental
it('warns in concurrent mode', () => {
expect(() => {
@@ -691,10 +731,14 @@ function runActTests(label, render, unmount, rerender) {
it('triggers fallbacks if available', async () => {
if (label !== 'legacy mode') {
- // FIXME: Support for Concurrent Root intentionally removed
- // from the public version of `act`. It will be added back in
- // a future major version, Concurrent Root officially released.
- // Consider skipping all non-Legacy tests in this suite until then.
+ // FIXME: Support for Blocking* and Concurrent Mode were
+ // intentionally removed from the public version of `act`. It will
+ // be added back in a future major version, before Blocking and and
+ // Concurrent Mode are officially released. Consider disabling all
+ // non-Legacy tests in this suite until then.
+ //
+ // *Blocking Mode actually does happen to work, though
+ // not "officially" since it's an unreleased feature.
return;
}
@@ -750,8 +794,10 @@ function runActTests(label, render, unmount, rerender) {
// In Concurrent Mode, refresh transitions delay indefinitely.
expect(document.querySelector('[data-test-id=spinner]')).toBeNull();
} else {
- // In Legacy Mode, all fallbacks are forced to display,
- // even during a refresh transition.
+ // In Legacy Mode and Blocking Mode, all fallbacks are forced to
+ // display, even during a refresh transition.
+ // TODO: Consider delaying indefinitely in Blocking Mode, to match
+ // Concurrent Mode semantics.
expect(
document.querySelector('[data-test-id=spinner]'),
).not.toBeNull();
diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js
index 79437e7f5c760..435b4989c1157 100644
--- a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js
+++ b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js
@@ -43,3 +43,22 @@ it('should warn when rendering in concurrent mode', () => {
ReactDOM.unstable_createRoot(document.createElement('div')).render();
}).toErrorDev([]);
});
+
+// @gate experimental
+it('should warn when rendering in blocking mode', () => {
+ expect(() => {
+ ReactDOM.unstable_createBlockingRoot(document.createElement('div')).render(
+ ,
+ );
+ }).toErrorDev(
+ 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
+ 'to guarantee consistent behaviour across tests and browsers.',
+ {withoutStack: true},
+ );
+ // does not warn twice
+ expect(() => {
+ ReactDOM.unstable_createBlockingRoot(document.createElement('div')).render(
+ ,
+ );
+ }).toErrorDev([]);
+});
diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js
index 7cff285a938d0..66ae001c3b27d 100644
--- a/packages/react-dom/src/client/ReactDOM.js
+++ b/packages/react-dom/src/client/ReactDOM.js
@@ -18,7 +18,7 @@ import {
unstable_renderSubtreeIntoContainer,
unmountComponentAtNode,
} from './ReactDOMLegacy';
-import {createRoot, isValidContainer} from './ReactDOMRoot';
+import {createRoot, createBlockingRoot, isValidContainer} from './ReactDOMRoot';
import {createEventHandle} from './ReactDOMEventHandle';
import {
@@ -201,6 +201,7 @@ export {
unmountComponentAtNode,
// exposeConcurrentModeAPIs
createRoot,
+ createBlockingRoot,
flushControlled as unstable_flushControlled,
scheduleHydration as unstable_scheduleHydration,
// Disabled behind disableUnstableRenderSubtreeIntoContainer
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index 56532d5d67488..62a72dc229618 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -51,17 +51,25 @@ import {
registerMutableSourceForHydration,
} from 'react-reconciler/src/ReactFiberReconciler';
import invariant from 'shared/invariant';
-import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
+import {
+ BlockingRoot,
+ ConcurrentRoot,
+ LegacyRoot,
+} from 'react-reconciler/src/ReactRootTags';
function ReactDOMRoot(container: Container, options: void | RootOptions) {
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
-function ReactDOMLegacyRoot(container: Container, options: void | RootOptions) {
- this._internalRoot = createRootImpl(container, LegacyRoot, options);
+function ReactDOMBlockingRoot(
+ container: Container,
+ tag: RootTag,
+ options: void | RootOptions,
+) {
+ this._internalRoot = createRootImpl(container, tag, options);
}
-ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
+ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot;
@@ -91,7 +99,7 @@ ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
updateContainer(children, root, null, null);
};
-ReactDOMRoot.prototype.unmount = ReactDOMLegacyRoot.prototype.unmount = function(): void {
+ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function(): void {
if (__DEV__) {
if (typeof arguments[0] === 'function') {
console.error(
@@ -161,11 +169,23 @@ export function createRoot(
return new ReactDOMRoot(container, options);
}
+export function createBlockingRoot(
+ container: Container,
+ options?: RootOptions,
+): RootType {
+ invariant(
+ isValidContainer(container),
+ 'createRoot(...): Target container is not a DOM element.',
+ );
+ warnIfReactDOMContainerInDEV(container);
+ return new ReactDOMBlockingRoot(container, BlockingRoot, options);
+}
+
export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
- return new ReactDOMLegacyRoot(container, options);
+ return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}
export function isValidContainer(node: mixed): boolean {
diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js
index c09fa2d8000f5..8305dd6d8641f 100644
--- a/packages/react-noop-renderer/src/ReactNoop.js
+++ b/packages/react-noop-renderer/src/ReactNoop.js
@@ -23,6 +23,7 @@ export const {
getPendingChildren,
getOrCreateRootContainer,
createRoot,
+ createBlockingRoot,
createLegacyRoot,
getChildrenAsJSX,
getPendingChildrenAsJSX,
diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js
index 97876990a9b57..c4a73cdfb81b4 100644
--- a/packages/react-noop-renderer/src/ReactNoopPersistent.js
+++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js
@@ -23,6 +23,7 @@ export const {
getPendingChildren,
getOrCreateRootContainer,
createRoot,
+ createBlockingRoot,
createLegacyRoot,
getChildrenAsJSX,
getPendingChildrenAsJSX,
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 9eef962f1339b..743470966b3f0 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -21,7 +21,11 @@ import type {RootTag} from 'react-reconciler/src/ReactRootTags';
import * as Scheduler from 'scheduler/unstable_mock';
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
-import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
+import {
+ ConcurrentRoot,
+ BlockingRoot,
+ LegacyRoot,
+} from 'react-reconciler/src/ReactRootTags';
import {
enableNativeEventPriorityInference,
@@ -752,6 +756,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
};
},
+ createBlockingRoot() {
+ const container = {
+ rootID: '' + idCounter++,
+ pendingChildren: [],
+ children: [],
+ };
+ const fiberRoot = NoopRenderer.createContainer(
+ container,
+ BlockingRoot,
+ false,
+ null,
+ null,
+ );
+ return {
+ _Scheduler: Scheduler,
+ render(children: ReactNodeList) {
+ NoopRenderer.updateContainer(children, fiberRoot, null, null);
+ },
+ getChildren() {
+ return getChildren(container);
+ },
+ getChildrenAsJSX() {
+ return getChildrenAsJSX(container);
+ },
+ };
+ },
+
createLegacyRoot() {
const container = {
rootID: '' + idCounter++,
diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js
index b386eac09f81e..f1a0f56b23409 100644
--- a/packages/react-reconciler/src/ReactFiber.new.js
+++ b/packages/react-reconciler/src/ReactFiber.new.js
@@ -26,7 +26,7 @@ import {
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
-import {ConcurrentRoot} from './ReactRootTags';
+import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
import {
IndeterminateComponent,
ClassComponent,
@@ -68,6 +68,7 @@ import {
ProfileMode,
StrictLegacyMode,
StrictEffectsMode,
+ BlockingMode,
} from './ReactTypeOfMode';
import {
REACT_FORWARD_REF_TYPE,
@@ -426,7 +427,25 @@ export function createHostRootFiber(
): Fiber {
let mode;
if (tag === ConcurrentRoot) {
- mode = ConcurrentMode;
+ mode = ConcurrentMode | BlockingMode;
+ if (strictModeLevelOverride !== null) {
+ if (strictModeLevelOverride >= 1) {
+ mode |= StrictLegacyMode;
+ }
+ if (enableStrictEffects) {
+ if (strictModeLevelOverride >= 2) {
+ mode |= StrictEffectsMode;
+ }
+ }
+ } else {
+ if (enableStrictEffects && createRootStrictEffectsByDefault) {
+ mode |= StrictLegacyMode | StrictEffectsMode;
+ } else {
+ mode |= StrictLegacyMode;
+ }
+ }
+ } else if (tag === BlockingRoot) {
+ mode = BlockingMode;
if (strictModeLevelOverride !== null) {
if (strictModeLevelOverride >= 1) {
mode |= StrictLegacyMode;
diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js
index 603feb345ff9a..6419fd6b261b3 100644
--- a/packages/react-reconciler/src/ReactFiber.old.js
+++ b/packages/react-reconciler/src/ReactFiber.old.js
@@ -26,7 +26,7 @@ import {
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
-import {ConcurrentRoot} from './ReactRootTags';
+import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
import {
IndeterminateComponent,
ClassComponent,
@@ -68,6 +68,7 @@ import {
ProfileMode,
StrictLegacyMode,
StrictEffectsMode,
+ BlockingMode,
} from './ReactTypeOfMode';
import {
REACT_FORWARD_REF_TYPE,
@@ -426,7 +427,25 @@ export function createHostRootFiber(
): Fiber {
let mode;
if (tag === ConcurrentRoot) {
- mode = ConcurrentMode;
+ mode = ConcurrentMode | BlockingMode;
+ if (strictModeLevelOverride !== null) {
+ if (strictModeLevelOverride >= 1) {
+ mode |= StrictLegacyMode;
+ }
+ if (enableStrictEffects) {
+ if (strictModeLevelOverride >= 2) {
+ mode |= StrictEffectsMode;
+ }
+ }
+ } else {
+ if (enableStrictEffects && createRootStrictEffectsByDefault) {
+ mode |= StrictLegacyMode | StrictEffectsMode;
+ } else {
+ mode |= StrictLegacyMode;
+ }
+ }
+ } else if (tag === BlockingRoot) {
+ mode = BlockingMode;
if (strictModeLevelOverride !== null) {
if (strictModeLevelOverride >= 1) {
mode |= StrictLegacyMode;
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index a4868edfd6434..6dffcc328927a 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -126,6 +126,7 @@ import {
NoMode,
ProfileMode,
StrictLegacyMode,
+ BlockingMode,
} from './ReactTypeOfMode';
import {
shouldSetTextContent,
@@ -603,6 +604,7 @@ function updateOffscreenComponent(
// Rendering a hidden tree.
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy sync mode, don't defer the subtree. Render it now.
+ // TODO: Figure out what we should do in Blocking mode.
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
@@ -2115,10 +2117,7 @@ function mountSuspenseFallbackChildren(
let primaryChildFragment;
let fallbackChildFragment;
- if (
- (mode & ConcurrentMode) === NoMode &&
- progressedPrimaryFragment !== null
- ) {
+ if ((mode & BlockingMode) === NoMode && progressedPrimaryFragment !== null) {
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
primaryChildFragment = progressedPrimaryFragment;
@@ -2190,7 +2189,7 @@ function updateSuspensePrimaryChildren(
children: primaryChildren,
},
);
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
primaryChildFragment.lanes = renderLanes;
}
primaryChildFragment.return = workInProgress;
@@ -2231,7 +2230,7 @@ function updateSuspenseFallbackChildren(
if (
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
- (mode & ConcurrentMode) === NoMode &&
+ (mode & BlockingMode) === NoMode &&
// Make sure we're on the second pass, i.e. the primary child fragment was
// already cloned. In legacy mode, the only case where this isn't true is
// when DevTools forces us to display a fallback; we skip the first render
@@ -2353,7 +2352,7 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating(
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
- if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
// We will have dropped the effect list which contains the
// deletion. We need to reconcile to delete the current child.
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
@@ -2369,12 +2368,12 @@ function mountDehydratedSuspenseComponent(
): null | Fiber {
// During the first pass, we'll bail out and not drill into the children.
// Instead, we'll leave the content in place and try to hydrate it later.
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
if (__DEV__) {
console.error(
'Cannot hydrate Suspense in legacy mode. Switch from ' +
'ReactDOM.hydrate(element, container) to ' +
- 'ReactDOM.createRoot(container, { hydrate: true })' +
+ 'ReactDOM.createBlockingRoot(container, { hydrate: true })' +
'.render(element) or remove the Suspense components from ' +
'the server rendered components.',
);
@@ -2427,7 +2426,7 @@ function updateDehydratedSuspenseComponent(
);
}
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
@@ -2832,7 +2831,7 @@ function updateSuspenseListComponent(
}
pushSuspenseContext(workInProgress, suspenseContext);
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
// In legacy mode, SuspenseList doesn't work so we just
// use make it a noop by treating it as the default revealOrder.
workInProgress.memoizedState = null;
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index c706200815d7c..7801afe9fa231 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -126,6 +126,7 @@ import {
NoMode,
ProfileMode,
StrictLegacyMode,
+ BlockingMode,
} from './ReactTypeOfMode';
import {
shouldSetTextContent,
@@ -603,6 +604,7 @@ function updateOffscreenComponent(
// Rendering a hidden tree.
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy sync mode, don't defer the subtree. Render it now.
+ // TODO: Figure out what we should do in Blocking mode.
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
@@ -2115,10 +2117,7 @@ function mountSuspenseFallbackChildren(
let primaryChildFragment;
let fallbackChildFragment;
- if (
- (mode & ConcurrentMode) === NoMode &&
- progressedPrimaryFragment !== null
- ) {
+ if ((mode & BlockingMode) === NoMode && progressedPrimaryFragment !== null) {
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
primaryChildFragment = progressedPrimaryFragment;
@@ -2190,7 +2189,7 @@ function updateSuspensePrimaryChildren(
children: primaryChildren,
},
);
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
primaryChildFragment.lanes = renderLanes;
}
primaryChildFragment.return = workInProgress;
@@ -2231,7 +2230,7 @@ function updateSuspenseFallbackChildren(
if (
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
- (mode & ConcurrentMode) === NoMode &&
+ (mode & BlockingMode) === NoMode &&
// Make sure we're on the second pass, i.e. the primary child fragment was
// already cloned. In legacy mode, the only case where this isn't true is
// when DevTools forces us to display a fallback; we skip the first render
@@ -2353,7 +2352,7 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating(
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
- if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
// We will have dropped the effect list which contains the
// deletion. We need to reconcile to delete the current child.
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
@@ -2369,12 +2368,12 @@ function mountDehydratedSuspenseComponent(
): null | Fiber {
// During the first pass, we'll bail out and not drill into the children.
// Instead, we'll leave the content in place and try to hydrate it later.
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
if (__DEV__) {
console.error(
'Cannot hydrate Suspense in legacy mode. Switch from ' +
'ReactDOM.hydrate(element, container) to ' +
- 'ReactDOM.createRoot(container, { hydrate: true })' +
+ 'ReactDOM.createBlockingRoot(container, { hydrate: true })' +
'.render(element) or remove the Suspense components from ' +
'the server rendered components.',
);
@@ -2427,7 +2426,7 @@ function updateDehydratedSuspenseComponent(
);
}
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
@@ -2832,7 +2831,7 @@ function updateSuspenseListComponent(
}
pushSuspenseContext(workInProgress, suspenseContext);
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
// In legacy mode, SuspenseList doesn't work so we just
// use make it a noop by treating it as the default revealOrder.
workInProgress.memoizedState = null;
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index 1dcc0ec9c87d1..47f65d2da6efc 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -55,7 +55,12 @@ import {
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
-import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
+import {
+ NoMode,
+ BlockingMode,
+ ConcurrentMode,
+ ProfileMode,
+} from './ReactTypeOfMode';
import {
Ref,
Update,
@@ -1054,10 +1059,12 @@ function completeWork(
}
if (nextDidTimeout && !prevDidTimeout) {
+ // If this subtree is running in blocking mode we can suspend,
+ // otherwise we won't suspend.
// 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) !== NoMode) {
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
// TODO: Move this back to throwException because this is too late
// if this is a large tree which is common for initial loads. We
// don't know if we should restart a render or not until we get
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index dc1112d3b88f6..df878d24e76a5 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -55,7 +55,12 @@ import {
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
-import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
+import {
+ NoMode,
+ BlockingMode,
+ ConcurrentMode,
+ ProfileMode,
+} from './ReactTypeOfMode';
import {
Ref,
Update,
@@ -1054,10 +1059,12 @@ function completeWork(
}
if (nextDidTimeout && !prevDidTimeout) {
+ // If this subtree is running in blocking mode we can suspend,
+ // otherwise we won't suspend.
// 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) !== NoMode) {
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
// TODO: Move this back to throwException because this is too late
// if this is a large tree which is common for initial loads. We
// don't know if we should restart a render or not until we get
diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js
index 80d62f25967ea..9d1fa5f82ed8e 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.new.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.new.js
@@ -34,7 +34,7 @@ import {
import {
NoMode,
- ConcurrentMode,
+ BlockingMode,
DebugTracingMode,
StrictEffectsMode,
} from './ReactTypeOfMode';
@@ -1829,7 +1829,7 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
const setId = mountState(id)[1];
- if ((currentlyRenderingFiber.mode & ConcurrentMode) === NoMode) {
+ if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
if (
__DEV__ &&
enableStrictEffects &&
diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js
index 2bdead48f33b5..61f1a17452e4b 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.old.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.old.js
@@ -34,7 +34,7 @@ import {
import {
NoMode,
- ConcurrentMode,
+ BlockingMode,
DebugTracingMode,
StrictEffectsMode,
} from './ReactTypeOfMode';
@@ -1829,7 +1829,7 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
const setId = mountState(id)[1];
- if ((currentlyRenderingFiber.mode & ConcurrentMode) === NoMode) {
+ if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
if (
__DEV__ &&
enableStrictEffects &&
diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js
index f06925e10fbe9..ae6a5bdb596f6 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.new.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.new.js
@@ -25,7 +25,7 @@ import {
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
-import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
+import {LegacyRoot, BlockingRoot, ConcurrentRoot} from './ReactRootTags';
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
@@ -73,6 +73,9 @@ function FiberRootNode(containerInfo, tag, hydrate) {
if (__DEV__) {
switch (tag) {
+ case BlockingRoot:
+ this._debugRootType = 'createBlockingRoot()';
+ break;
case ConcurrentRoot:
this._debugRootType = 'createRoot()';
break;
diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js
index 712803920ebb2..0c0d45c098720 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.old.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.old.js
@@ -25,7 +25,7 @@ import {
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {initializeUpdateQueue} from './ReactUpdateQueue.old';
-import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
+import {LegacyRoot, BlockingRoot, ConcurrentRoot} from './ReactRootTags';
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
@@ -73,6 +73,9 @@ function FiberRootNode(containerInfo, tag, hydrate) {
if (__DEV__) {
switch (tag) {
+ case BlockingRoot:
+ this._debugRootType = 'createBlockingRoot()';
+ break;
case ConcurrentRoot:
this._debugRootType = 'createRoot()';
break;
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index 0f7dd89263d10..af199f0aa8e81 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -34,7 +34,7 @@ import {
ForceUpdateForLegacySuspense,
} from './ReactFiberFlags';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
-import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
import {
enableDebugTracing,
enableSchedulingProfiler,
@@ -214,7 +214,7 @@ function throwException(
// A legacy mode Suspense quirk, only relevant to hook components.
const tag = sourceFiber.tag;
if (
- (sourceFiber.mode & ConcurrentMode) === NoMode &&
+ (sourceFiber.mode & BlockingMode) === NoMode &&
(tag === FunctionComponent ||
tag === ForwardRef ||
tag === SimpleMemoComponent)
@@ -255,13 +255,13 @@ function throwException(
wakeables.add(wakeable);
}
- // If the boundary is in legacy mode, we should *not*
+ // If the boundary is outside of blocking mode, we should *not*
// suspend the commit. Pretend as if the suspended component rendered
// null and keep rendering. In the commit phase, we'll schedule a
// subsequent synchronous update to re-render the Suspense.
//
// Note: It doesn't matter whether the component that suspended was
- // inside a concurrent mode tree. If the Suspense is outside of it, we
+ // inside a blocking mode tree. If the Suspense is outside of it, we
// should *not* suspend the commit.
//
// If the suspense boundary suspended itself suspended, we don't have to
@@ -269,7 +269,7 @@ function throwException(
// directly do a second pass over the fallback in this render and
// pretend we meant to render that directly.
if (
- (workInProgress.mode & ConcurrentMode) === NoMode &&
+ (workInProgress.mode & BlockingMode) === NoMode &&
workInProgress !== returnFiber
) {
workInProgress.flags |= DidCapture;
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index f7ceb05202cb5..6ae53d02fc5ce 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -34,7 +34,7 @@ import {
ForceUpdateForLegacySuspense,
} from './ReactFiberFlags';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
-import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
import {
enableDebugTracing,
enableSchedulingProfiler,
@@ -214,7 +214,7 @@ function throwException(
// A legacy mode Suspense quirk, only relevant to hook components.
const tag = sourceFiber.tag;
if (
- (sourceFiber.mode & ConcurrentMode) === NoMode &&
+ (sourceFiber.mode & BlockingMode) === NoMode &&
(tag === FunctionComponent ||
tag === ForwardRef ||
tag === SimpleMemoComponent)
@@ -255,13 +255,13 @@ function throwException(
wakeables.add(wakeable);
}
- // If the boundary is in legacy mode, we should *not*
+ // If the boundary is outside of blocking mode, we should *not*
// suspend the commit. Pretend as if the suspended component rendered
// null and keep rendering. In the commit phase, we'll schedule a
// subsequent synchronous update to re-render the Suspense.
//
// Note: It doesn't matter whether the component that suspended was
- // inside a concurrent mode tree. If the Suspense is outside of it, we
+ // inside a blocking mode tree. If the Suspense is outside of it, we
// should *not* suspend the commit.
//
// If the suspense boundary suspended itself suspended, we don't have to
@@ -269,7 +269,7 @@ function throwException(
// directly do a second pass over the fallback in this render and
// pretend we meant to render that directly.
if (
- (workInProgress.mode & ConcurrentMode) === NoMode &&
+ (workInProgress.mode & BlockingMode) === NoMode &&
workInProgress !== returnFiber
) {
workInProgress.flags |= DidCapture;
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index b404837cb25d0..7caee07872517 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -106,6 +106,7 @@ import {
NoMode,
StrictLegacyMode,
ProfileMode,
+ BlockingMode,
ConcurrentMode,
} from './ReactTypeOfMode';
import {
@@ -391,7 +392,7 @@ export function getCurrentTime() {
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
- if ((mode & ConcurrentMode) === NoMode) {
+ if ((mode & BlockingMode) === NoMode) {
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -482,7 +483,7 @@ function requestRetryLane(fiber: Fiber) {
// Special cases
const mode = fiber.mode;
- if ((mode & ConcurrentMode) === NoMode) {
+ if ((mode & BlockingMode) === NoMode) {
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -676,7 +677,7 @@ export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
// Requires some refactoring. Not a big deal though since it's rare for
// concurrent apps to have more than a single root.
workInProgressRoot !== null &&
- (fiber.mode & ConcurrentMode) !== NoMode &&
+ (fiber.mode & BlockingMode) !== NoMode &&
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
// then don't treat this as an interleaved update. This pattern is
// accompanied by a warning but we haven't fully deprecated it yet. We can
@@ -2624,7 +2625,7 @@ function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
return;
}
- if (!(fiber.mode & ConcurrentMode)) {
+ if (!(fiber.mode & (BlockingMode | ConcurrentMode))) {
return;
}
@@ -3003,7 +3004,7 @@ export function warnIfUnmockedScheduler(fiber: Fiber) {
didWarnAboutUnmockedScheduler === false &&
Scheduler.unstable_flushAllWithoutAsserting === undefined
) {
- if (fiber.mode & ConcurrentMode) {
+ if (fiber.mode & BlockingMode || fiber.mode & ConcurrentMode) {
didWarnAboutUnmockedScheduler = true;
console.error(
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 0a366eece7321..1467929ce4597 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -106,6 +106,7 @@ import {
NoMode,
StrictLegacyMode,
ProfileMode,
+ BlockingMode,
ConcurrentMode,
} from './ReactTypeOfMode';
import {
@@ -391,7 +392,7 @@ export function getCurrentTime() {
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
- if ((mode & ConcurrentMode) === NoMode) {
+ if ((mode & BlockingMode) === NoMode) {
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -482,7 +483,7 @@ function requestRetryLane(fiber: Fiber) {
// Special cases
const mode = fiber.mode;
- if ((mode & ConcurrentMode) === NoMode) {
+ if ((mode & BlockingMode) === NoMode) {
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -676,7 +677,7 @@ export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
// Requires some refactoring. Not a big deal though since it's rare for
// concurrent apps to have more than a single root.
workInProgressRoot !== null &&
- (fiber.mode & ConcurrentMode) !== NoMode &&
+ (fiber.mode & BlockingMode) !== NoMode &&
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
// then don't treat this as an interleaved update. This pattern is
// accompanied by a warning but we haven't fully deprecated it yet. We can
@@ -2624,7 +2625,7 @@ function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
return;
}
- if (!(fiber.mode & ConcurrentMode)) {
+ if (!(fiber.mode & (BlockingMode | ConcurrentMode))) {
return;
}
@@ -3003,7 +3004,7 @@ export function warnIfUnmockedScheduler(fiber: Fiber) {
didWarnAboutUnmockedScheduler === false &&
Scheduler.unstable_flushAllWithoutAsserting === undefined
) {
- if (fiber.mode & ConcurrentMode) {
+ if (fiber.mode & BlockingMode || fiber.mode & ConcurrentMode) {
didWarnAboutUnmockedScheduler = true;
console.error(
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
diff --git a/packages/react-reconciler/src/ReactRootTags.js b/packages/react-reconciler/src/ReactRootTags.js
index cda44d6e1ed51..409f4bd931a76 100644
--- a/packages/react-reconciler/src/ReactRootTags.js
+++ b/packages/react-reconciler/src/ReactRootTags.js
@@ -7,7 +7,8 @@
* @flow
*/
-export type RootTag = 0 | 1;
+export type RootTag = 0 | 1 | 2;
export const LegacyRoot = 0;
-export const ConcurrentRoot = 1;
+export const BlockingRoot = 1;
+export const ConcurrentRoot = 2;
diff --git a/packages/react-reconciler/src/ReactTypeOfMode.js b/packages/react-reconciler/src/ReactTypeOfMode.js
index 466363fabd4e8..a6499be7aca11 100644
--- a/packages/react-reconciler/src/ReactTypeOfMode.js
+++ b/packages/react-reconciler/src/ReactTypeOfMode.js
@@ -10,9 +10,10 @@
export type TypeOfMode = number;
export const NoMode = /* */ 0b000000;
-// TODO: Remove ConcurrentMode by reading from the root tag instead
-export const ConcurrentMode = /* */ 0b000001;
-export const ProfileMode = /* */ 0b000010;
-export const DebugTracingMode = /* */ 0b000100;
-export const StrictLegacyMode = /* */ 0b001000;
-export const StrictEffectsMode = /* */ 0b010000;
+// TODO: Remove BlockingMode and ConcurrentMode by reading from the root tag instead
+export const BlockingMode = /* */ 0b000001;
+export const ConcurrentMode = /* */ 0b000010;
+export const ProfileMode = /* */ 0b000100;
+export const DebugTracingMode = /* */ 0b001000;
+export const StrictLegacyMode = /* */ 0b010000;
+export const StrictEffectsMode = /* */ 0b100000;
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..c6af59fbd915c
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js
@@ -0,0 +1,167 @@
+let React;
+let ReactFeatureFlags;
+let ReactNoop;
+let Scheduler;
+let ReactCache;
+let Suspense;
+let TextResource;
+
+describe('ReactBlockingMode', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ Scheduler = require('scheduler');
+ ReactCache = require('react-cache');
+ Suspense = React.Suspense;
+
+ TextResource = ReactCache.unstable_createResource(
+ ([text, ms = 0]) => {
+ return new Promise((resolve, reject) =>
+ setTimeout(() => {
+ Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
+ resolve(text);
+ }, ms),
+ );
+ },
+ ([text, ms]) => text,
+ );
+ });
+
+ function Text(props) {
+ Scheduler.unstable_yieldValue(props.text);
+ return props.text;
+ }
+
+ function AsyncText(props) {
+ const text = props.text;
+ try {
+ TextResource.read([props.text, props.ms]);
+ Scheduler.unstable_yieldValue(text);
+ return props.text;
+ } catch (promise) {
+ if (typeof promise.then === 'function') {
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
+ } else {
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
+ }
+ throw promise;
+ }
+ }
+
+ it('updates flush without yielding in the next event', () => {
+ const root = ReactNoop.createBlockingRoot();
+
+ 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.unstable_yieldValue('Layout effect');
+ });
+ return ;
+ }
+
+ const root = ReactNoop.createBlockingRoot();
+ root.render();
+ expect(root).toMatchRenderedOutput(null);
+
+ expect(Scheduler).toFlushExpired(['Hi', 'Layout effect']);
+ expect(root).toMatchRenderedOutput('Hi');
+ });
+
+ it('uses proper Suspense semantics, not legacy ones', async () => {
+ const root = ReactNoop.createBlockingRoot();
+ root.render(
+ }>
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ expect(Scheduler).toFlushExpired(['A', 'Suspend! [B]', 'C', 'Loading...']);
+ // In Legacy Mode, A and B would mount in a hidden primary tree. In Batched
+ // and Concurrent Mode, nothing in the primary tree should mount. But the
+ // fallback should mount immediately.
+ expect(root).toMatchRenderedOutput('Loading...');
+
+ await jest.advanceTimersByTime(1000);
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
+ expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
+ expect(root).toMatchRenderedOutput(
+ <>
+ A
+ B
+ C
+ >,
+ );
+ });
+
+ it('flushSync does not flush batched work', () => {
+ const {useState, forwardRef, useImperativeHandle} = React;
+ const root = ReactNoop.createBlockingRoot();
+
+ const Foo = forwardRef(({label}, ref) => {
+ const [step, setStep] = useState(0);
+ useImperativeHandle(ref, () => ({setStep}));
+ return ;
+ });
+
+ const foo1 = React.createRef(null);
+ const foo2 = React.createRef(null);
+ root.render(
+ <>
+
+
+ >,
+ );
+
+ // Mount
+ expect(Scheduler).toFlushExpired(['A0', 'B0']);
+ expect(root).toMatchRenderedOutput('A0B0');
+
+ // Schedule a batched update to the first sibling
+ ReactNoop.batchedUpdates(() => foo1.current.setStep(1));
+
+ // Before it flushes, update the second sibling inside flushSync
+ ReactNoop.batchedUpdates(() =>
+ ReactNoop.flushSync(() => {
+ foo2.current.setStep(1);
+ }),
+ );
+
+ // Only the second update should have flushed synchronously
+ expect(Scheduler).toHaveYielded(['B1']);
+ expect(root).toMatchRenderedOutput('A0B1');
+
+ // Now flush the first update
+ expect(Scheduler).toFlushExpired(['A1']);
+ expect(root).toMatchRenderedOutput('A1B1');
+ });
+});
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index a14c36bfce2bb..613fe89d63279 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -1870,6 +1870,10 @@ describe('ReactIncrementalErrorHandling', () => {
const root = ReactNoop.createRoot();
root.render('Error when completing root');
expect(Scheduler).toFlushAndThrow('Error when completing root');
+
+ const blockingRoot = ReactNoop.createBlockingRoot();
+ blockingRoot.render('Error when completing root');
+ expect(Scheduler).toFlushAndThrow('Error when completing root');
});
}
});
diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
index 2c6914fbfc0e9..b3f14e6ad1efc 100644
--- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
@@ -123,4 +123,46 @@ describe('ReactOffscreen', () => {
>,
);
});
+
+ // @gate experimental
+ it('does not defer in blocking mode', async () => {
+ let setState;
+ function Foo() {
+ const [state, _setState] = useState('A');
+ setState = _setState;
+ return ;
+ }
+
+ const root = ReactNoop.createBlockingRoot();
+ await ReactNoop.act(async () => {
+ root.render(
+ <>
+
+
+
+
+ >,
+ );
+ // Should not defer the hidden tree
+ expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']);
+ });
+ expect(root).toMatchRenderedOutput(
+ <>
+
+
+ >,
+ );
+
+ // Test that the children can be updated
+ await ReactNoop.act(async () => {
+ setState('B');
+ });
+ expect(Scheduler).toHaveYielded(['B']);
+ expect(root).toMatchRenderedOutput(
+ <>
+
+
+ >,
+ );
+ });
});
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
index c67edf8a15632..eabcb41be0e6d 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
@@ -170,6 +170,13 @@ describe('ReactSuspenseFuzz', () => {
expect(legacyOutput).toEqual(expectedOutput);
ReactNoop.renderLegacySyncRoot(null);
+ resetCache();
+ const batchedBlockingRoot = ReactNoop.createBlockingRoot();
+ batchedBlockingRoot.render(children);
+ resolveAllTasks();
+ const batchedSyncOutput = batchedBlockingRoot.getChildrenAsJSX();
+ expect(batchedSyncOutput).toEqual(expectedOutput);
+
resetCache();
const concurrentRoot = ReactNoop.createRoot();
concurrentRoot.render(children);
diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js
index 20c83a46dfaac..cb567340d3983 100644
--- a/packages/react/src/__tests__/ReactStrictMode-test.internal.js
+++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js
@@ -66,6 +66,23 @@ describe('ReactStrictMode', () => {
]);
});
+ // @gate experimental
+ it('should support overriding default via createBlockingRoot option', () => {
+ act(() => {
+ const container = document.createElement('div');
+ const root = ReactDOM.createBlockingRoot(container, {
+ unstable_strictModeLevel: 0,
+ });
+ root.render();
+ });
+
+ expect(log).toEqual([
+ 'A: render',
+ 'A: useLayoutEffect mount',
+ 'A: useEffect mount',
+ ]);
+ });
+
// @gate experimental
it('should disable strict mode if level 0 is specified', () => {
act(() => {
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index b6c7a2b93d0f2..201546e822b20 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -27,7 +27,7 @@ export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
// this feature flag only impacts StrictEffectsMode.
export const enableStrictEffects = false;
-// If TRUE, trees rendered with createRoot will be StrictEffectsMode.
+// If TRUE, trees rendered with createRoot (and createBlockingRoot) APIs will be StrictEffectsMode.
// If FALSE, these trees will be StrictLegacyMode.
export const createRootStrictEffectsByDefault = false;