@samitouri / QOS-React-2 / commits / ee43263572

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