Revert "Remove blocking mode and blocking root (#20888)" (#20916)
This reverts commit 553440bd1578ef71982c4a10e2cc8c462f33d9be.
Andrew Clark committed
Mar 2, 2021 at 14:51 UTC
ee432635724d5a50301448016caa137ac3c0a7a2
36 files changed
+531
-81
packages/react-dom/index.classic.fb.js
+2
@@ -30,6 +30,8 @@ export {
30
unmountComponentAtNode,
31
createRoot,
32
createRoot as unstable_createRoot,
33
+ createBlockingRoot,
34
+ createBlockingRoot as unstable_createBlockingRoot,
35
unstable_flushControlled,
36
unstable_scheduleHydration,
37
unstable_runWithPriority,
packages/react-dom/index.experimental.js
+1
@@ -20,6 +20,7 @@ export {
20
unmountComponentAtNode,
21
// exposeConcurrentModeAPIs
22
createRoot as unstable_createRoot,
23
+ createBlockingRoot as unstable_createBlockingRoot,
24
unstable_flushControlled,
25
unstable_scheduleHydration,
26
// DO NOT USE: Temporarily exposing this to migrate off of Scheduler.runWithPriority.
packages/react-dom/index.js
+2
@@ -21,6 +21,8 @@ export {
21
unmountComponentAtNode,
22
createRoot,
23
createRoot as unstable_createRoot,
24
+ createBlockingRoot,
25
+ createBlockingRoot as unstable_createBlockingRoot,
26
unstable_flushControlled,
27
unstable_scheduleHydration,
28
unstable_runWithPriority,
packages/react-dom/index.modern.fb.js
+2
@@ -15,6 +15,8 @@ export {
15
version,
16
createRoot,
17
createRoot as unstable_createRoot,
18
+ createBlockingRoot,
19
+ createBlockingRoot as unstable_createBlockingRoot,
20
unstable_flushControlled,
21
unstable_scheduleHydration,
22
unstable_runWithPriority,
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+27
@@ -593,6 +593,33 @@ describe('ReactDOMFiberAsync', () => {
593
expect(containerC.textContent).toEqual('Finished');
594
});
595
596
+ describe('createBlockingRoot', () => {
597
+ // @gate experimental
598
+ it('updates flush without yielding in the next event', () => {
599
+ const root = ReactDOM.unstable_createBlockingRoot(container);
600
+
601
+ function Text(props) {
602
+ Scheduler.unstable_yieldValue(props.text);
603
+ return props.text;
604
+ }
605
+
606
+ root.render(
607
+ <>
608
+ <Text text="A" />
609
+ <Text text="B" />
610
+ <Text text="C" />
611
+ </>,
612
+ );
613
+
614
+ // Nothing should have rendered yet
615
+ expect(container.textContent).toEqual('');
616
+
617
+ // Everything should render immediately in the next event
618
+ expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
619
+ expect(container.textContent).toEqual('ABC');
620
+ });
621
+ });
622
+
623
// @gate experimental
624
it('unmounted roots should never clear newer root content from a container', () => {
625
const ref = React.createRef();
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+1
-1
@@ -352,7 +352,7 @@ describe('ReactDOMServerPartialHydration', () => {
352
}).toErrorDev(
353
'Warning: Cannot hydrate Suspense in legacy mode. Switch from ' +
354
'ReactDOM.hydrate(element, container) to ' +
355
- 'ReactDOM.createRoot(container, { hydrate: true })' +
355
+ 'ReactDOM.createBlockingRoot(container, { hydrate: true })' +
356
'.render(element) or remove the Suspense components from the server ' +
357
'rendered components.' +
358
'\n in Suspense (at **)' +
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+1
-1
@@ -127,7 +127,7 @@ describe('ReactDOMServerSuspense', () => {
127
expect(divB.textContent).toBe('B');
128
129
act(() => {
130
- const root = ReactDOM.createRoot(parent, {hydrate: true});
130
+ const root = ReactDOM.createBlockingRoot(parent, {hydrate: true});
131
root.render(example);
132
});
133
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+52
-6
@@ -72,6 +72,33 @@ describe('ReactTestUtils.act()', () => {
72
73
runActTests('legacy mode', renderLegacy, unmountLegacy, rerenderLegacy);
74
75
+ // and then in blocking mode
76
+ if (__EXPERIMENTAL__) {
77
+ let blockingRoot = null;
78
+ const renderBatched = (el, dom) => {
79
+ blockingRoot = ReactDOM.unstable_createBlockingRoot(dom);
80
+ blockingRoot.render(el);
81
+ };
82
+
83
+ const unmountBatched = dom => {
84
+ if (blockingRoot !== null) {
85
+ blockingRoot.unmount();
86
+ blockingRoot = null;
87
+ }
88
+ };
89
+
90
+ const rerenderBatched = el => {
91
+ blockingRoot.render(el);
92
+ };
93
+
94
+ runActTests(
95
+ 'blocking mode',
96
+ renderBatched,
97
+ unmountBatched,
98
+ rerenderBatched,
99
+ );
100
+ }
101
+
102
describe('unacted effects', () => {
103
function App() {
104
React.useEffect(() => {}, []);
@@ -97,6 +124,19 @@ describe('ReactTestUtils.act()', () => {
124
]);
125
});
126
127
+ // @gate experimental
128
+ it('warns in blocking mode', () => {
129
+ expect(() => {
130
+ const root = ReactDOM.unstable_createBlockingRoot(
131
+ document.createElement('div'),
132
+ );
133
+ root.render(<App />);
134
+ Scheduler.unstable_flushAll();
135
+ }).toErrorDev([
136
+ 'An update to App ran an effect, but was not wrapped in act(...)',
137
+ ]);
138
+ });
139
+
140
// @gate experimental
141
it('warns in concurrent mode', () => {
142
expect(() => {
@@ -691,10 +731,14 @@ function runActTests(label, render, unmount, rerender) {
731
732
it('triggers fallbacks if available', async () => {
733
if (label !== 'legacy mode') {
694
- // FIXME: Support for Concurrent Root intentionally removed
695
- // from the public version of `act`. It will be added back in
696
- // a future major version, Concurrent Root officially released.
697
- // Consider skipping all non-Legacy tests in this suite until then.
734
+ // FIXME: Support for Blocking* and Concurrent Mode were
735
+ // intentionally removed from the public version of `act`. It will
736
+ // be added back in a future major version, before Blocking and and
737
+ // Concurrent Mode are officially released. Consider disabling all
738
+ // non-Legacy tests in this suite until then.
739
+ //
740
+ // *Blocking Mode actually does happen to work, though
741
+ // not "officially" since it's an unreleased feature.
742
return;
743
}
744
@@ -750,8 +794,10 @@ function runActTests(label, render, unmount, rerender) {
794
// In Concurrent Mode, refresh transitions delay indefinitely.
795
expect(document.querySelector('[data-test-id=spinner]')).toBeNull();
796
} else {
753
- // In Legacy Mode, all fallbacks are forced to display,
754
- // even during a refresh transition.
797
+ // In Legacy Mode and Blocking Mode, all fallbacks are forced to
798
+ // display, even during a refresh transition.
799
+ // TODO: Consider delaying indefinitely in Blocking Mode, to match
800
+ // Concurrent Mode semantics.
801
expect(
802
document.querySelector('[data-test-id=spinner]'),
803
).not.toBeNull();
packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js
+19
@@ -43,3 +43,22 @@ it('should warn when rendering in concurrent mode', () => {
43
ReactDOM.unstable_createRoot(document.createElement('div')).render(<App />);
44
}).toErrorDev([]);
45
});
46
+
47
+// @gate experimental
48
+it('should warn when rendering in blocking mode', () => {
49
+ expect(() => {
50
+ ReactDOM.unstable_createBlockingRoot(document.createElement('div')).render(
51
+ <App />,
52
+ );
53
+ }).toErrorDev(
54
+ 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
55
+ 'to guarantee consistent behaviour across tests and browsers.',
56
+ {withoutStack: true},
57
+ );
58
+ // does not warn twice
59
+ expect(() => {
60
+ ReactDOM.unstable_createBlockingRoot(document.createElement('div')).render(
61
+ <App />,
62
+ );
63
+ }).toErrorDev([]);
64
+});
packages/react-dom/src/client/ReactDOM.js
+2
-1
@@ -18,7 +18,7 @@ import {
18
unstable_renderSubtreeIntoContainer,
19
unmountComponentAtNode,
20
} from './ReactDOMLegacy';
21
-import {createRoot, isValidContainer} from './ReactDOMRoot';
21
+import {createRoot, createBlockingRoot, isValidContainer} from './ReactDOMRoot';
22
import {createEventHandle} from './ReactDOMEventHandle';
23
24
import {
@@ -201,6 +201,7 @@ export {
201
unmountComponentAtNode,
202
// exposeConcurrentModeAPIs
203
createRoot,
204
+ createBlockingRoot,
205
flushControlled as unstable_flushControlled,
206
scheduleHydration as unstable_scheduleHydration,
207
// Disabled behind disableUnstableRenderSubtreeIntoContainer
packages/react-dom/src/client/ReactDOMRoot.js
+26
-6
@@ -51,17 +51,25 @@ import {
51
registerMutableSourceForHydration,
52
} from 'react-reconciler/src/ReactFiberReconciler';
53
import invariant from 'shared/invariant';
54
-import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
54
+import {
55
+ BlockingRoot,
56
+ ConcurrentRoot,
57
+ LegacyRoot,
58
+} from 'react-reconciler/src/ReactRootTags';
59
60
function ReactDOMRoot(container: Container, options: void | RootOptions) {
61
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
62
}
63
60
-function ReactDOMLegacyRoot(container: Container, options: void | RootOptions) {
61
- this._internalRoot = createRootImpl(container, LegacyRoot, options);
64
+function ReactDOMBlockingRoot(
65
+ container: Container,
66
+ tag: RootTag,
67
+ options: void | RootOptions,
68
+) {
69
+ this._internalRoot = createRootImpl(container, tag, options);
70
}
71
64
-ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
72
+ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
73
children: ReactNodeList,
74
): void {
75
const root = this._internalRoot;
@@ -91,7 +99,7 @@ ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
99
updateContainer(children, root, null, null);
100
};
101
94
-ReactDOMRoot.prototype.unmount = ReactDOMLegacyRoot.prototype.unmount = function(): void {
102
+ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function(): void {
103
if (__DEV__) {
104
if (typeof arguments[0] === 'function') {
105
console.error(
@@ -161,11 +169,23 @@ export function createRoot(
169
return new ReactDOMRoot(container, options);
170
}
171
172
+export function createBlockingRoot(
173
+ container: Container,
174
+ options?: RootOptions,
175
+): RootType {
176
+ invariant(
177
+ isValidContainer(container),
178
+ 'createRoot(...): Target container is not a DOM element.',
179
+ );
180
+ warnIfReactDOMContainerInDEV(container);
181
+ return new ReactDOMBlockingRoot(container, BlockingRoot, options);
182
+}
183
+
184
export function createLegacyRoot(
185
container: Container,
186
options?: RootOptions,
187
): RootType {
168
- return new ReactDOMLegacyRoot(container, options);
188
+ return new ReactDOMBlockingRoot(container, LegacyRoot, options);
189
}
190
191
export function isValidContainer(node: mixed): boolean {
packages/react-noop-renderer/src/ReactNoop.js
+1
@@ -23,6 +23,7 @@ export const {
23
getPendingChildren,
24
getOrCreateRootContainer,
25
createRoot,
26
+ createBlockingRoot,
27
createLegacyRoot,
28
getChildrenAsJSX,
29
getPendingChildrenAsJSX,
packages/react-noop-renderer/src/ReactNoopPersistent.js
+1
@@ -23,6 +23,7 @@ export const {
23
getPendingChildren,
24
getOrCreateRootContainer,
25
createRoot,
26
+ createBlockingRoot,
27
createLegacyRoot,
28
getChildrenAsJSX,
29
getPendingChildrenAsJSX,
packages/react-noop-renderer/src/createReactNoop.js
+32
-1
@@ -21,7 +21,11 @@ import type {RootTag} from 'react-reconciler/src/ReactRootTags';
21
22
import * as Scheduler from 'scheduler/unstable_mock';
23
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
24
-import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
24
+import {
25
+ ConcurrentRoot,
26
+ BlockingRoot,
27
+ LegacyRoot,
28
+} from 'react-reconciler/src/ReactRootTags';
29
30
import {
31
enableNativeEventPriorityInference,
@@ -752,6 +756,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
756
};
757
},
758
759
+ createBlockingRoot() {
760
+ const container = {
761
+ rootID: '' + idCounter++,
762
+ pendingChildren: [],
763
+ children: [],
764
+ };
765
+ const fiberRoot = NoopRenderer.createContainer(
766
+ container,
767
+ BlockingRoot,
768
+ false,
769
+ null,
770
+ null,
771
+ );
772
+ return {
773
+ _Scheduler: Scheduler,
774
+ render(children: ReactNodeList) {
775
+ NoopRenderer.updateContainer(children, fiberRoot, null, null);
776
+ },
777
+ getChildren() {
778
+ return getChildren(container);
779
+ },
780
+ getChildrenAsJSX() {
781
+ return getChildrenAsJSX(container);
782
+ },
783
+ };
784
+ },
785
+
786
createLegacyRoot() {
787
const container = {
788
rootID: '' + idCounter++,
packages/react-reconciler/src/ReactFiber.new.js
+21
-2
@@ -26,7 +26,7 @@ import {
26
enableScopeAPI,
27
} from 'shared/ReactFeatureFlags';
28
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
29
-import {ConcurrentRoot} from './ReactRootTags';
29
+import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
30
import {
31
IndeterminateComponent,
32
ClassComponent,
@@ -68,6 +68,7 @@ import {
68
ProfileMode,
69
StrictLegacyMode,
70
StrictEffectsMode,
71
+ BlockingMode,
72
} from './ReactTypeOfMode';
73
import {
74
REACT_FORWARD_REF_TYPE,
@@ -426,7 +427,25 @@ export function createHostRootFiber(
427
): Fiber {
428
let mode;
429
if (tag === ConcurrentRoot) {
429
- mode = ConcurrentMode;
430
+ mode = ConcurrentMode | BlockingMode;
431
+ if (strictModeLevelOverride !== null) {
432
+ if (strictModeLevelOverride >= 1) {
433
+ mode |= StrictLegacyMode;
434
+ }
435
+ if (enableStrictEffects) {
436
+ if (strictModeLevelOverride >= 2) {
437
+ mode |= StrictEffectsMode;
438
+ }
439
+ }
440
+ } else {
441
+ if (enableStrictEffects && createRootStrictEffectsByDefault) {
442
+ mode |= StrictLegacyMode | StrictEffectsMode;
443
+ } else {
444
+ mode |= StrictLegacyMode;
445
+ }
446
+ }
447
+ } else if (tag === BlockingRoot) {
448
+ mode = BlockingMode;
449
if (strictModeLevelOverride !== null) {
450
if (strictModeLevelOverride >= 1) {
451
mode |= StrictLegacyMode;
packages/react-reconciler/src/ReactFiber.old.js
+21
-2
@@ -26,7 +26,7 @@ import {
26
enableScopeAPI,
27
} from 'shared/ReactFeatureFlags';
28
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
29
-import {ConcurrentRoot} from './ReactRootTags';
29
+import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
30
import {
31
IndeterminateComponent,
32
ClassComponent,
@@ -68,6 +68,7 @@ import {
68
ProfileMode,
69
StrictLegacyMode,
70
StrictEffectsMode,
71
+ BlockingMode,
72
} from './ReactTypeOfMode';
73
import {
74
REACT_FORWARD_REF_TYPE,
@@ -426,7 +427,25 @@ export function createHostRootFiber(
427
): Fiber {
428
let mode;
429
if (tag === ConcurrentRoot) {
429
- mode = ConcurrentMode;
430
+ mode = ConcurrentMode | BlockingMode;
431
+ if (strictModeLevelOverride !== null) {
432
+ if (strictModeLevelOverride >= 1) {
433
+ mode |= StrictLegacyMode;
434
+ }
435
+ if (enableStrictEffects) {
436
+ if (strictModeLevelOverride >= 2) {
437
+ mode |= StrictEffectsMode;
438
+ }
439
+ }
440
+ } else {
441
+ if (enableStrictEffects && createRootStrictEffectsByDefault) {
442
+ mode |= StrictLegacyMode | StrictEffectsMode;
443
+ } else {
444
+ mode |= StrictLegacyMode;
445
+ }
446
+ }
447
+ } else if (tag === BlockingRoot) {
448
+ mode = BlockingMode;
449
if (strictModeLevelOverride !== null) {
450
if (strictModeLevelOverride >= 1) {
451
mode |= StrictLegacyMode;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+10
-11
@@ -126,6 +126,7 @@ import {
126
NoMode,
127
ProfileMode,
128
StrictLegacyMode,
129
+ BlockingMode,
130
} from './ReactTypeOfMode';
131
import {
132
shouldSetTextContent,
@@ -603,6 +604,7 @@ function updateOffscreenComponent(
604
// Rendering a hidden tree.
605
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
606
// In legacy sync mode, don't defer the subtree. Render it now.
607
+ // TODO: Figure out what we should do in Blocking mode.
608
const nextState: OffscreenState = {
609
baseLanes: NoLanes,
610
cachePool: null,
@@ -2115,10 +2117,7 @@ function mountSuspenseFallbackChildren(
2117
2118
let primaryChildFragment;
2119
let fallbackChildFragment;
2118
- if (
2119
- (mode & ConcurrentMode) === NoMode &&
2120
- progressedPrimaryFragment !== null
2121
- ) {
2120
+ if ((mode & BlockingMode) === NoMode && progressedPrimaryFragment !== null) {
2121
// In legacy mode, we commit the primary tree as if it successfully
2122
// completed, even though it's in an inconsistent state.
2123
primaryChildFragment = progressedPrimaryFragment;
@@ -2190,7 +2189,7 @@ function updateSuspensePrimaryChildren(
2189
children: primaryChildren,
2190
},
2191
);
2193
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2192
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2193
primaryChildFragment.lanes = renderLanes;
2194
}
2195
primaryChildFragment.return = workInProgress;
@@ -2231,7 +2230,7 @@ function updateSuspenseFallbackChildren(
2230
if (
2231
// In legacy mode, we commit the primary tree as if it successfully
2232
// completed, even though it's in an inconsistent state.
2234
- (mode & ConcurrentMode) === NoMode &&
2233
+ (mode & BlockingMode) === NoMode &&
2234
// Make sure we're on the second pass, i.e. the primary child fragment was
2235
// already cloned. In legacy mode, the only case where this isn't true is
2236
// when DevTools forces us to display a fallback; we skip the first render
@@ -2353,7 +2352,7 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating(
2352
primaryChildFragment.sibling = fallbackChildFragment;
2353
workInProgress.child = primaryChildFragment;
2354
2356
- if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
2355
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
2356
// We will have dropped the effect list which contains the
2357
// deletion. We need to reconcile to delete the current child.
2358
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
@@ -2369,12 +2368,12 @@ function mountDehydratedSuspenseComponent(
2368
): null | Fiber {
2369
// During the first pass, we'll bail out and not drill into the children.
2370
// Instead, we'll leave the content in place and try to hydrate it later.
2372
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2371
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2372
if (__DEV__) {
2373
console.error(
2374
'Cannot hydrate Suspense in legacy mode. Switch from ' +
2375
'ReactDOM.hydrate(element, container) to ' +
2377
- 'ReactDOM.createRoot(container, { hydrate: true })' +
2376
+ 'ReactDOM.createBlockingRoot(container, { hydrate: true })' +
2377
'.render(element) or remove the Suspense components from ' +
2378
'the server rendered components.',
2379
);
@@ -2427,7 +2426,7 @@ function updateDehydratedSuspenseComponent(
2426
);
2427
}
2428
2430
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2429
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2430
return retrySuspenseComponentWithoutHydrating(
2431
current,
2432
workInProgress,
@@ -2832,7 +2831,7 @@ function updateSuspenseListComponent(
2831
}
2832
pushSuspenseContext(workInProgress, suspenseContext);
2833
2835
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2834
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2835
// In legacy mode, SuspenseList doesn't work so we just
2836
// use make it a noop by treating it as the default revealOrder.
2837
workInProgress.memoizedState = null;
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+10
-11
@@ -126,6 +126,7 @@ import {
126
NoMode,
127
ProfileMode,
128
StrictLegacyMode,
129
+ BlockingMode,
130
} from './ReactTypeOfMode';
131
import {
132
shouldSetTextContent,
@@ -603,6 +604,7 @@ function updateOffscreenComponent(
604
// Rendering a hidden tree.
605
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
606
// In legacy sync mode, don't defer the subtree. Render it now.
607
+ // TODO: Figure out what we should do in Blocking mode.
608
const nextState: OffscreenState = {
609
baseLanes: NoLanes,
610
cachePool: null,
@@ -2115,10 +2117,7 @@ function mountSuspenseFallbackChildren(
2117
2118
let primaryChildFragment;
2119
let fallbackChildFragment;
2118
- if (
2119
- (mode & ConcurrentMode) === NoMode &&
2120
- progressedPrimaryFragment !== null
2121
- ) {
2120
+ if ((mode & BlockingMode) === NoMode && progressedPrimaryFragment !== null) {
2121
// In legacy mode, we commit the primary tree as if it successfully
2122
// completed, even though it's in an inconsistent state.
2123
primaryChildFragment = progressedPrimaryFragment;
@@ -2190,7 +2189,7 @@ function updateSuspensePrimaryChildren(
2189
children: primaryChildren,
2190
},
2191
);
2193
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2192
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2193
primaryChildFragment.lanes = renderLanes;
2194
}
2195
primaryChildFragment.return = workInProgress;
@@ -2231,7 +2230,7 @@ function updateSuspenseFallbackChildren(
2230
if (
2231
// In legacy mode, we commit the primary tree as if it successfully
2232
// completed, even though it's in an inconsistent state.
2234
- (mode & ConcurrentMode) === NoMode &&
2233
+ (mode & BlockingMode) === NoMode &&
2234
// Make sure we're on the second pass, i.e. the primary child fragment was
2235
// already cloned. In legacy mode, the only case where this isn't true is
2236
// when DevTools forces us to display a fallback; we skip the first render
@@ -2353,7 +2352,7 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating(
2352
primaryChildFragment.sibling = fallbackChildFragment;
2353
workInProgress.child = primaryChildFragment;
2354
2356
- if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
2355
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
2356
// We will have dropped the effect list which contains the
2357
// deletion. We need to reconcile to delete the current child.
2358
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
@@ -2369,12 +2368,12 @@ function mountDehydratedSuspenseComponent(
2368
): null | Fiber {
2369
// During the first pass, we'll bail out and not drill into the children.
2370
// Instead, we'll leave the content in place and try to hydrate it later.
2372
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2371
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2372
if (__DEV__) {
2373
console.error(
2374
'Cannot hydrate Suspense in legacy mode. Switch from ' +
2375
'ReactDOM.hydrate(element, container) to ' +
2377
- 'ReactDOM.createRoot(container, { hydrate: true })' +
2376
+ 'ReactDOM.createBlockingRoot(container, { hydrate: true })' +
2377
'.render(element) or remove the Suspense components from ' +
2378
'the server rendered components.',
2379
);
@@ -2427,7 +2426,7 @@ function updateDehydratedSuspenseComponent(
2426
);
2427
}
2428
2430
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2429
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2430
return retrySuspenseComponentWithoutHydrating(
2431
current,
2432
workInProgress,
@@ -2832,7 +2831,7 @@ function updateSuspenseListComponent(
2831
}
2832
pushSuspenseContext(workInProgress, suspenseContext);
2833
2835
- if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2834
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2835
// In legacy mode, SuspenseList doesn't work so we just
2836
// use make it a noop by treating it as the default revealOrder.
2837
workInProgress.memoizedState = null;
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+9
-2
@@ -55,7 +55,12 @@ import {
55
LegacyHiddenComponent,
56
CacheComponent,
57
} from './ReactWorkTags';
58
-import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
58
+import {
59
+ NoMode,
60
+ BlockingMode,
61
+ ConcurrentMode,
62
+ ProfileMode,
63
+} from './ReactTypeOfMode';
64
import {
65
Ref,
66
Update,
@@ -1054,10 +1059,12 @@ function completeWork(
1059
}
1060
1061
if (nextDidTimeout && !prevDidTimeout) {
1062
+ // If this subtree is running in blocking mode we can suspend,
1063
+ // otherwise we won't suspend.
1064
// TODO: This will still suspend a synchronous tree if anything
1065
// in the concurrent tree already suspended during this render.
1066
// This is a known bug.
1060
- if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
1067
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
1068
// TODO: Move this back to throwException because this is too late
1069
// if this is a large tree which is common for initial loads. We
1070
// don't know if we should restart a render or not until we get
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+9
-2
@@ -55,7 +55,12 @@ import {
55
LegacyHiddenComponent,
56
CacheComponent,
57
} from './ReactWorkTags';
58
-import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
58
+import {
59
+ NoMode,
60
+ BlockingMode,
61
+ ConcurrentMode,
62
+ ProfileMode,
63
+} from './ReactTypeOfMode';
64
import {
65
Ref,
66
Update,
@@ -1054,10 +1059,12 @@ function completeWork(
1059
}
1060
1061
if (nextDidTimeout && !prevDidTimeout) {
1062
+ // If this subtree is running in blocking mode we can suspend,
1063
+ // otherwise we won't suspend.
1064
// TODO: This will still suspend a synchronous tree if anything
1065
// in the concurrent tree already suspended during this render.
1066
// This is a known bug.
1060
- if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
1067
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
1068
// TODO: Move this back to throwException because this is too late
1069
// if this is a large tree which is common for initial loads. We
1070
// don't know if we should restart a render or not until we get
packages/react-reconciler/src/ReactFiberHooks.new.js
+2
-2
@@ -34,7 +34,7 @@ import {
34
35
import {
36
NoMode,
37
- ConcurrentMode,
37
+ BlockingMode,
38
DebugTracingMode,
39
StrictEffectsMode,
40
} from './ReactTypeOfMode';
@@ -1829,7 +1829,7 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
1829
1830
const setId = mountState(id)[1];
1831
1832
- if ((currentlyRenderingFiber.mode & ConcurrentMode) === NoMode) {
1832
+ if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1833
if (
1834
__DEV__ &&
1835
enableStrictEffects &&
packages/react-reconciler/src/ReactFiberHooks.old.js
+2
-2
@@ -34,7 +34,7 @@ import {
34
35
import {
36
NoMode,
37
- ConcurrentMode,
37
+ BlockingMode,
38
DebugTracingMode,
39
StrictEffectsMode,
40
} from './ReactTypeOfMode';
@@ -1829,7 +1829,7 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
1829
1830
const setId = mountState(id)[1];
1831
1832
- if ((currentlyRenderingFiber.mode & ConcurrentMode) === NoMode) {
1832
+ if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1833
if (
1834
__DEV__ &&
1835
enableStrictEffects &&
packages/react-reconciler/src/ReactFiberRoot.new.js
+4
-1
@@ -25,7 +25,7 @@ import {
25
} from 'shared/ReactFeatureFlags';
26
import {unstable_getThreadID} from 'scheduler/tracing';
27
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
28
-import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
28
+import {LegacyRoot, BlockingRoot, ConcurrentRoot} from './ReactRootTags';
29
30
function FiberRootNode(containerInfo, tag, hydrate) {
31
this.tag = tag;
@@ -73,6 +73,9 @@ function FiberRootNode(containerInfo, tag, hydrate) {
73
74
if (__DEV__) {
75
switch (tag) {
76
+ case BlockingRoot:
77
+ this._debugRootType = 'createBlockingRoot()';
78
+ break;
79
case ConcurrentRoot:
80
this._debugRootType = 'createRoot()';
81
break;
packages/react-reconciler/src/ReactFiberRoot.old.js
+4
-1
@@ -25,7 +25,7 @@ import {
25
} from 'shared/ReactFeatureFlags';
26
import {unstable_getThreadID} from 'scheduler/tracing';
27
import {initializeUpdateQueue} from './ReactUpdateQueue.old';
28
-import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
28
+import {LegacyRoot, BlockingRoot, ConcurrentRoot} from './ReactRootTags';
29
30
function FiberRootNode(containerInfo, tag, hydrate) {
31
this.tag = tag;
@@ -73,6 +73,9 @@ function FiberRootNode(containerInfo, tag, hydrate) {
73
74
if (__DEV__) {
75
switch (tag) {
76
+ case BlockingRoot:
77
+ this._debugRootType = 'createBlockingRoot()';
78
+ break;
79
case ConcurrentRoot:
80
this._debugRootType = 'createRoot()';
81
break;
packages/react-reconciler/src/ReactFiberThrow.new.js
+5
-5
@@ -34,7 +34,7 @@ import {
34
ForceUpdateForLegacySuspense,
35
} from './ReactFiberFlags';
36
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
37
-import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
37
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
38
import {
39
enableDebugTracing,
40
enableSchedulingProfiler,
@@ -214,7 +214,7 @@ function throwException(
214
// A legacy mode Suspense quirk, only relevant to hook components.
215
const tag = sourceFiber.tag;
216
if (
217
- (sourceFiber.mode & ConcurrentMode) === NoMode &&
217
+ (sourceFiber.mode & BlockingMode) === NoMode &&
218
(tag === FunctionComponent ||
219
tag === ForwardRef ||
220
tag === SimpleMemoComponent)
@@ -255,13 +255,13 @@ function throwException(
255
wakeables.add(wakeable);
256
}
257
258
- // If the boundary is in legacy mode, we should *not*
258
+ // If the boundary is outside of blocking mode, we should *not*
259
// suspend the commit. Pretend as if the suspended component rendered
260
// null and keep rendering. In the commit phase, we'll schedule a
261
// subsequent synchronous update to re-render the Suspense.
262
//
263
// Note: It doesn't matter whether the component that suspended was
264
- // inside a concurrent mode tree. If the Suspense is outside of it, we
264
+ // inside a blocking mode tree. If the Suspense is outside of it, we
265
// should *not* suspend the commit.
266
//
267
// If the suspense boundary suspended itself suspended, we don't have to
@@ -269,7 +269,7 @@ function throwException(
269
// directly do a second pass over the fallback in this render and
270
// pretend we meant to render that directly.
271
if (
272
- (workInProgress.mode & ConcurrentMode) === NoMode &&
272
+ (workInProgress.mode & BlockingMode) === NoMode &&
273
workInProgress !== returnFiber
274
) {
275
workInProgress.flags |= DidCapture;
packages/react-reconciler/src/ReactFiberThrow.old.js
+5
-5
@@ -34,7 +34,7 @@ import {
34
ForceUpdateForLegacySuspense,
35
} from './ReactFiberFlags';
36
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
37
-import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
37
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
38
import {
39
enableDebugTracing,
40
enableSchedulingProfiler,
@@ -214,7 +214,7 @@ function throwException(
214
// A legacy mode Suspense quirk, only relevant to hook components.
215
const tag = sourceFiber.tag;
216
if (
217
- (sourceFiber.mode & ConcurrentMode) === NoMode &&
217
+ (sourceFiber.mode & BlockingMode) === NoMode &&
218
(tag === FunctionComponent ||
219
tag === ForwardRef ||
220
tag === SimpleMemoComponent)
@@ -255,13 +255,13 @@ function throwException(
255
wakeables.add(wakeable);
256
}
257
258
- // If the boundary is in legacy mode, we should *not*
258
+ // If the boundary is outside of blocking mode, we should *not*
259
// suspend the commit. Pretend as if the suspended component rendered
260
// null and keep rendering. In the commit phase, we'll schedule a
261
// subsequent synchronous update to re-render the Suspense.
262
//
263
// Note: It doesn't matter whether the component that suspended was
264
- // inside a concurrent mode tree. If the Suspense is outside of it, we
264
+ // inside a blocking mode tree. If the Suspense is outside of it, we
265
// should *not* suspend the commit.
266
//
267
// If the suspense boundary suspended itself suspended, we don't have to
@@ -269,7 +269,7 @@ function throwException(
269
// directly do a second pass over the fallback in this render and
270
// pretend we meant to render that directly.
271
if (
272
- (workInProgress.mode & ConcurrentMode) === NoMode &&
272
+ (workInProgress.mode & BlockingMode) === NoMode &&
273
workInProgress !== returnFiber
274
) {
275
workInProgress.flags |= DidCapture;
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+6
-5
@@ -106,6 +106,7 @@ import {
106
NoMode,
107
StrictLegacyMode,
108
ProfileMode,
109
+ BlockingMode,
110
ConcurrentMode,
111
} from './ReactTypeOfMode';
112
import {
@@ -391,7 +392,7 @@ export function getCurrentTime() {
392
export function requestUpdateLane(fiber: Fiber): Lane {
393
// Special cases
394
const mode = fiber.mode;
394
- if ((mode & ConcurrentMode) === NoMode) {
395
+ if ((mode & BlockingMode) === NoMode) {
396
return (SyncLane: Lane);
397
} else if ((mode & ConcurrentMode) === NoMode) {
398
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -482,7 +483,7 @@ function requestRetryLane(fiber: Fiber) {
483
484
// Special cases
485
const mode = fiber.mode;
485
- if ((mode & ConcurrentMode) === NoMode) {
486
+ if ((mode & BlockingMode) === NoMode) {
487
return (SyncLane: Lane);
488
} else if ((mode & ConcurrentMode) === NoMode) {
489
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -676,7 +677,7 @@ export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
677
// Requires some refactoring. Not a big deal though since it's rare for
678
// concurrent apps to have more than a single root.
679
workInProgressRoot !== null &&
679
- (fiber.mode & ConcurrentMode) !== NoMode &&
680
+ (fiber.mode & BlockingMode) !== NoMode &&
681
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
682
// then don't treat this as an interleaved update. This pattern is
683
// accompanied by a warning but we haven't fully deprecated it yet. We can
@@ -2624,7 +2625,7 @@ function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
2625
return;
2626
}
2627
2627
- if (!(fiber.mode & ConcurrentMode)) {
2628
+ if (!(fiber.mode & (BlockingMode | ConcurrentMode))) {
2629
return;
2630
}
2631
@@ -3003,7 +3004,7 @@ export function warnIfUnmockedScheduler(fiber: Fiber) {
3004
didWarnAboutUnmockedScheduler === false &&
3005
Scheduler.unstable_flushAllWithoutAsserting === undefined
3006
) {
3006
- if (fiber.mode & ConcurrentMode) {
3007
+ if (fiber.mode & BlockingMode || fiber.mode & ConcurrentMode) {
3008
didWarnAboutUnmockedScheduler = true;
3009
console.error(
3010
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+6
-5
@@ -106,6 +106,7 @@ import {
106
NoMode,
107
StrictLegacyMode,
108
ProfileMode,
109
+ BlockingMode,
110
ConcurrentMode,
111
} from './ReactTypeOfMode';
112
import {
@@ -391,7 +392,7 @@ export function getCurrentTime() {
392
export function requestUpdateLane(fiber: Fiber): Lane {
393
// Special cases
394
const mode = fiber.mode;
394
- if ((mode & ConcurrentMode) === NoMode) {
395
+ if ((mode & BlockingMode) === NoMode) {
396
return (SyncLane: Lane);
397
} else if ((mode & ConcurrentMode) === NoMode) {
398
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -482,7 +483,7 @@ function requestRetryLane(fiber: Fiber) {
483
484
// Special cases
485
const mode = fiber.mode;
485
- if ((mode & ConcurrentMode) === NoMode) {
486
+ if ((mode & BlockingMode) === NoMode) {
487
return (SyncLane: Lane);
488
} else if ((mode & ConcurrentMode) === NoMode) {
489
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
@@ -676,7 +677,7 @@ export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
677
// Requires some refactoring. Not a big deal though since it's rare for
678
// concurrent apps to have more than a single root.
679
workInProgressRoot !== null &&
679
- (fiber.mode & ConcurrentMode) !== NoMode &&
680
+ (fiber.mode & BlockingMode) !== NoMode &&
681
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
682
// then don't treat this as an interleaved update. This pattern is
683
// accompanied by a warning but we haven't fully deprecated it yet. We can
@@ -2624,7 +2625,7 @@ function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
2625
return;
2626
}
2627
2627
- if (!(fiber.mode & ConcurrentMode)) {
2628
+ if (!(fiber.mode & (BlockingMode | ConcurrentMode))) {
2629
return;
2630
}
2631
@@ -3003,7 +3004,7 @@ export function warnIfUnmockedScheduler(fiber: Fiber) {
3004
didWarnAboutUnmockedScheduler === false &&
3005
Scheduler.unstable_flushAllWithoutAsserting === undefined
3006
) {
3006
- if (fiber.mode & ConcurrentMode) {
3007
+ if (fiber.mode & BlockingMode || fiber.mode & ConcurrentMode) {
3008
didWarnAboutUnmockedScheduler = true;
3009
console.error(
3010
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
packages/react-reconciler/src/ReactRootTags.js
+3
-2
@@ -7,7 +7,8 @@
7
* @flow
8
*/
9
10
-export type RootTag = 0 | 1;
10
+export type RootTag = 0 | 1 | 2;
11
12
export const LegacyRoot = 0;
13
-export const ConcurrentRoot = 1;
13
+export const BlockingRoot = 1;
14
+export const ConcurrentRoot = 2;
packages/react-reconciler/src/ReactTypeOfMode.js
+7
-6
@@ -10,9 +10,10 @@
10
export type TypeOfMode = number;
11
12
export const NoMode = /* */ 0b000000;
13
-// TODO: Remove ConcurrentMode by reading from the root tag instead
14
-export const ConcurrentMode = /* */ 0b000001;
15
-export const ProfileMode = /* */ 0b000010;
16
-export const DebugTracingMode = /* */ 0b000100;
17
-export const StrictLegacyMode = /* */ 0b001000;
18
-export const StrictEffectsMode = /* */ 0b010000;
13
+// TODO: Remove BlockingMode and ConcurrentMode by reading from the root tag instead
14
+export const BlockingMode = /* */ 0b000001;
15
+export const ConcurrentMode = /* */ 0b000010;
16
+export const ProfileMode = /* */ 0b000100;
17
+export const DebugTracingMode = /* */ 0b001000;
18
+export const StrictLegacyMode = /* */ 0b010000;
19
+export const StrictEffectsMode = /* */ 0b100000;
packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js
new
+167
@@ -0,0 +1,167 @@
1
+let React;
2
+let ReactFeatureFlags;
3
+let ReactNoop;
4
+let Scheduler;
5
+let ReactCache;
6
+let Suspense;
7
+let TextResource;
8
+
9
+describe('ReactBlockingMode', () => {
10
+ beforeEach(() => {
11
+ jest.resetModules();
12
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
13
+
14
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
15
+ React = require('react');
16
+ ReactNoop = require('react-noop-renderer');
17
+ Scheduler = require('scheduler');
18
+ ReactCache = require('react-cache');
19
+ Suspense = React.Suspense;
20
+
21
+ TextResource = ReactCache.unstable_createResource(
22
+ ([text, ms = 0]) => {
23
+ return new Promise((resolve, reject) =>
24
+ setTimeout(() => {
25
+ Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
26
+ resolve(text);
27
+ }, ms),
28
+ );
29
+ },
30
+ ([text, ms]) => text,
31
+ );
32
+ });
33
+
34
+ function Text(props) {
35
+ Scheduler.unstable_yieldValue(props.text);
36
+ return props.text;
37
+ }
38
+
39
+ function AsyncText(props) {
40
+ const text = props.text;
41
+ try {
42
+ TextResource.read([props.text, props.ms]);
43
+ Scheduler.unstable_yieldValue(text);
44
+ return props.text;
45
+ } catch (promise) {
46
+ if (typeof promise.then === 'function') {
47
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
48
+ } else {
49
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
50
+ }
51
+ throw promise;
52
+ }
53
+ }
54
+
55
+ it('updates flush without yielding in the next event', () => {
56
+ const root = ReactNoop.createBlockingRoot();
57
+
58
+ root.render(
59
+ <>
60
+ <Text text="A" />
61
+ <Text text="B" />
62
+ <Text text="C" />
63
+ </>,
64
+ );
65
+
66
+ // Nothing should have rendered yet
67
+ expect(root).toMatchRenderedOutput(null);
68
+
69
+ // Everything should render immediately in the next event
70
+ expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
71
+ expect(root).toMatchRenderedOutput('ABC');
72
+ });
73
+
74
+ it('layout updates flush synchronously in same event', () => {
75
+ const {useLayoutEffect} = React;
76
+
77
+ function App() {
78
+ useLayoutEffect(() => {
79
+ Scheduler.unstable_yieldValue('Layout effect');
80
+ });
81
+ return <Text text="Hi" />;
82
+ }
83
+
84
+ const root = ReactNoop.createBlockingRoot();
85
+ root.render(<App />);
86
+ expect(root).toMatchRenderedOutput(null);
87
+
88
+ expect(Scheduler).toFlushExpired(['Hi', 'Layout effect']);
89
+ expect(root).toMatchRenderedOutput('Hi');
90
+ });
91
+
92
+ it('uses proper Suspense semantics, not legacy ones', async () => {
93
+ const root = ReactNoop.createBlockingRoot();
94
+ root.render(
95
+ <Suspense fallback={<Text text="Loading..." />}>
96
+ <span>
97
+ <Text text="A" />
98
+ </span>
99
+ <span>
100
+ <AsyncText text="B" />
101
+ </span>
102
+ <span>
103
+ <Text text="C" />
104
+ </span>
105
+ </Suspense>,
106
+ );
107
+
108
+ expect(Scheduler).toFlushExpired(['A', 'Suspend! [B]', 'C', 'Loading...']);
109
+ // In Legacy Mode, A and B would mount in a hidden primary tree. In Batched
110
+ // and Concurrent Mode, nothing in the primary tree should mount. But the
111
+ // fallback should mount immediately.
112
+ expect(root).toMatchRenderedOutput('Loading...');
113
+
114
+ await jest.advanceTimersByTime(1000);
115
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
116
+ expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
117
+ expect(root).toMatchRenderedOutput(
118
+ <>
119
+ <span>A</span>
120
+ <span>B</span>
121
+ <span>C</span>
122
+ </>,
123
+ );
124
+ });
125
+
126
+ it('flushSync does not flush batched work', () => {
127
+ const {useState, forwardRef, useImperativeHandle} = React;
128
+ const root = ReactNoop.createBlockingRoot();
129
+
130
+ const Foo = forwardRef(({label}, ref) => {
131
+ const [step, setStep] = useState(0);
132
+ useImperativeHandle(ref, () => ({setStep}));
133
+ return <Text text={label + step} />;
134
+ });
135
+
136
+ const foo1 = React.createRef(null);
137
+ const foo2 = React.createRef(null);
138
+ root.render(
139
+ <>
140
+ <Foo label="A" ref={foo1} />
141
+ <Foo label="B" ref={foo2} />
142
+ </>,
143
+ );
144
+
145
+ // Mount
146
+ expect(Scheduler).toFlushExpired(['A0', 'B0']);
147
+ expect(root).toMatchRenderedOutput('A0B0');
148
+
149
+ // Schedule a batched update to the first sibling
150
+ ReactNoop.batchedUpdates(() => foo1.current.setStep(1));
151
+
152
+ // Before it flushes, update the second sibling inside flushSync
153
+ ReactNoop.batchedUpdates(() =>
154
+ ReactNoop.flushSync(() => {
155
+ foo2.current.setStep(1);
156
+ }),
157
+ );
158
+
159
+ // Only the second update should have flushed synchronously
160
+ expect(Scheduler).toHaveYielded(['B1']);
161
+ expect(root).toMatchRenderedOutput('A0B1');
162
+
163
+ // Now flush the first update
164
+ expect(Scheduler).toFlushExpired(['A1']);
165
+ expect(root).toMatchRenderedOutput('A1B1');
166
+ });
167
+});
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+4
@@ -1870,6 +1870,10 @@ describe('ReactIncrementalErrorHandling', () => {
1870
const root = ReactNoop.createRoot();
1871
root.render('Error when completing root');
1872
expect(Scheduler).toFlushAndThrow('Error when completing root');
1873
+
1874
+ const blockingRoot = ReactNoop.createBlockingRoot();
1875
+ blockingRoot.render('Error when completing root');
1876
+ expect(Scheduler).toFlushAndThrow('Error when completing root');
1877
});
1878
}
1879
});
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+42
@@ -123,4 +123,46 @@ describe('ReactOffscreen', () => {
123
</>,
124
);
125
});
126
+
127
+ // @gate experimental
128
+ it('does not defer in blocking mode', async () => {
129
+ let setState;
130
+ function Foo() {
131
+ const [state, _setState] = useState('A');
132
+ setState = _setState;
133
+ return <Text text={state} />;
134
+ }
135
+
136
+ const root = ReactNoop.createBlockingRoot();
137
+ await ReactNoop.act(async () => {
138
+ root.render(
139
+ <>
140
+ <LegacyHidden mode="hidden">
141
+ <Foo />
142
+ </LegacyHidden>
143
+ <Text text="Outside" />
144
+ </>,
145
+ );
146
+ // Should not defer the hidden tree
147
+ expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']);
148
+ });
149
+ expect(root).toMatchRenderedOutput(
150
+ <>
151
+ <span prop="A" />
152
+ <span prop="Outside" />
153
+ </>,
154
+ );
155
+
156
+ // Test that the children can be updated
157
+ await ReactNoop.act(async () => {
158
+ setState('B');
159
+ });
160
+ expect(Scheduler).toHaveYielded(['B']);
161
+ expect(root).toMatchRenderedOutput(
162
+ <>
163
+ <span prop="B" />
164
+ <span prop="Outside" />
165
+ </>,
166
+ );
167
+ });
168
});
packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
+7
@@ -170,6 +170,13 @@ describe('ReactSuspenseFuzz', () => {
170
expect(legacyOutput).toEqual(expectedOutput);
171
ReactNoop.renderLegacySyncRoot(null);
172
173
+ resetCache();
174
+ const batchedBlockingRoot = ReactNoop.createBlockingRoot();
175
+ batchedBlockingRoot.render(children);
176
+ resolveAllTasks();
177
+ const batchedSyncOutput = batchedBlockingRoot.getChildrenAsJSX();
178
+ expect(batchedSyncOutput).toEqual(expectedOutput);
179
+
180
resetCache();
181
const concurrentRoot = ReactNoop.createRoot();
182
concurrentRoot.render(children);
packages/react/src/__tests__/ReactStrictMode-test.internal.js
+17
@@ -66,6 +66,23 @@ describe('ReactStrictMode', () => {
66
]);
67
});
68
69
+ // @gate experimental
70
+ it('should support overriding default via createBlockingRoot option', () => {
71
+ act(() => {
72
+ const container = document.createElement('div');
73
+ const root = ReactDOM.createBlockingRoot(container, {
74
+ unstable_strictModeLevel: 0,
75
+ });
76
+ root.render(<Component label="A" />);
77
+ });
78
+
79
+ expect(log).toEqual([
80
+ 'A: render',
81
+ 'A: useLayoutEffect mount',
82
+ 'A: useEffect mount',
83
+ ]);
84
+ });
85
+
86
// @gate experimental
87
it('should disable strict mode if level 0 is specified', () => {
88
act(() => {
packages/shared/ReactFeatureFlags.js
+1
-1
@@ -27,7 +27,7 @@ export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
27
// this feature flag only impacts StrictEffectsMode.
28
export const enableStrictEffects = false;
29
30
-// If TRUE, trees rendered with createRoot will be StrictEffectsMode.
30
+// If TRUE, trees rendered with createRoot (and createBlockingRoot) APIs will be StrictEffectsMode.
31
// If FALSE, these trees will be StrictLegacyMode.
32
export const createRootStrictEffectsByDefault = false;
33