Add ReactDOM.unstable_createSyncRoot (#15504)
* Add ReactDOM.unstable_createSyncRoot - `ReactDOM.unstable_createRoot` creates a Concurrent Mode root. - `ReactDOM.unstable_createSyncRoot` creates a Batched Mode root. It does not support `createBatch`. - `ReactDOM.render` creates a Legacy Mode root. It will eventually be deprecated and possibly moved to a separate entry point, like `react-dom/legacy`. * Rename internal ReactRoot types
Andrew Clark committed
May 13, 2019 at 15:30 UTC
283ce532042c195d27996a556c04142ccf93f567
3 files changed
+174
-119
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
+31
@@ -661,6 +661,37 @@ describe('ReactDOMFiberAsync', () => {
661
});
662
});
663
664
+ describe('createSyncRoot', () => {
665
+ it('updates flush without yielding in the next event', () => {
666
+ const root = ReactDOM.unstable_createSyncRoot(container);
667
+
668
+ function Text(props) {
669
+ Scheduler.yieldValue(props.text);
670
+ return props.text;
671
+ }
672
+
673
+ root.render(
674
+ <React.Fragment>
675
+ <Text text="A" />
676
+ <Text text="B" />
677
+ <Text text="C" />
678
+ </React.Fragment>,
679
+ );
680
+
681
+ // Nothing should have rendered yet
682
+ expect(container.textContent).toEqual('');
683
+
684
+ // Everything should render immediately in the next event
685
+ expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
686
+ expect(container.textContent).toEqual('ABC');
687
+ });
688
+
689
+ it('does not support createBatch', () => {
690
+ const root = ReactDOM.unstable_createSyncRoot(container);
691
+ expect(root.createBatch).toBe(undefined);
692
+ });
693
+ });
694
+
695
describe('Disable yielding', () => {
696
beforeEach(() => {
697
jest.resetModules();
packages/react-dom/src/client/ReactDOM.js
+71
-59
@@ -53,7 +53,7 @@ import {
53
accumulateTwoPhaseDispatches,
54
accumulateDirectDispatches,
55
} from 'events/EventPropagators';
56
-import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
56
+import {LegacyRoot, ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
57
import {has as hasInstance} from 'shared/ReactInstanceMap';
58
import ReactVersion from 'shared/ReactVersion';
59
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -159,11 +159,11 @@ setRestoreImplementation(restoreControlledState);
159
160
export type DOMContainer =
161
| (Element & {
162
- _reactRootContainer: ?Root,
162
+ _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
163
_reactHasBeenPassedToCreateRootDEV: ?boolean,
164
})
165
| (Document & {
166
- _reactRootContainer: ?Root,
166
+ _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
167
_reactHasBeenPassedToCreateRootDEV: ?boolean,
168
});
169
@@ -175,7 +175,7 @@ type Batch = FiberRootBatch & {
175
// The ReactRoot constructor is hoisted but the prototype methods are not. If
176
// we move ReactRoot to be above ReactBatch, the inverse error occurs.
177
// $FlowFixMe Hoisting issue.
178
- _root: Root,
178
+ _root: _ReactRoot | _ReactSyncRoot,
179
_hasChildren: boolean,
180
_children: ReactNodeList,
181
@@ -183,20 +183,18 @@ type Batch = FiberRootBatch & {
183
_didComplete: boolean,
184
};
185
186
-type Root = {
186
+type _ReactSyncRoot = {
187
render(children: ReactNodeList, callback: ?() => mixed): Work,
188
unmount(callback: ?() => mixed): Work,
189
- legacy_renderSubtreeIntoContainer(
190
- parentComponent: ?React$Component<any, any>,
191
- children: ReactNodeList,
192
- callback: ?() => mixed,
193
- ): Work,
194
- createBatch(): Batch,
189
190
_internalRoot: FiberRoot,
191
};
192
199
-function ReactBatch(root: ReactRoot) {
193
+type _ReactRoot = _ReactSyncRoot & {
194
+ createBatch(): Batch,
195
+};
196
+
197
+function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
198
const expirationTime = computeUniqueAsyncExpiration();
199
this._expirationTime = expirationTime;
200
this._root = root;
@@ -363,11 +361,22 @@ ReactWork.prototype._onCommit = function(): void {
361
}
362
};
363
366
-function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
364
+function ReactSyncRoot(
365
+ container: DOMContainer,
366
+ tag: RootTag,
367
+ hydrate: boolean,
368
+) {
369
+ // Tag is either LegacyRoot or Concurrent Root
370
const root = createContainer(container, tag, hydrate);
371
this._internalRoot = root;
372
}
370
-ReactRoot.prototype.render = function(
373
+
374
+function ReactRoot(container: DOMContainer, hydrate: boolean) {
375
+ const root = createContainer(container, ConcurrentRoot, hydrate);
376
+ this._internalRoot = root;
377
+}
378
+
379
+ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
380
children: ReactNodeList,
381
callback: ?() => mixed,
382
): Work {
@@ -383,22 +392,8 @@ ReactRoot.prototype.render = function(
392
updateContainer(children, root, null, work._onCommit);
393
return work;
394
};
386
-ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work {
387
- const root = this._internalRoot;
388
- const work = new ReactWork();
389
- callback = callback === undefined ? null : callback;
390
- if (__DEV__) {
391
- warnOnInvalidCallback(callback, 'render');
392
- }
393
- if (callback !== null) {
394
- work.then(callback);
395
- }
396
- updateContainer(null, root, null, work._onCommit);
397
- return work;
398
-};
399
-ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
400
- parentComponent: ?React$Component<any, any>,
401
- children: ReactNodeList,
395
+
396
+ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
397
callback: ?() => mixed,
398
): Work {
399
const root = this._internalRoot;
@@ -410,9 +405,11 @@ ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
405
if (callback !== null) {
406
work.then(callback);
407
}
413
- updateContainer(children, root, parentComponent, work._onCommit);
408
+ updateContainer(null, root, null, work._onCommit);
409
return work;
410
};
411
+
412
+// Sync roots cannot create batches. Only concurrent ones.
413
ReactRoot.prototype.createBatch = function(): Batch {
414
const batch = new ReactBatch(this);
415
const expirationTime = batch._expirationTime;
@@ -492,7 +489,7 @@ let warnedAboutHydrateAPI = false;
489
function legacyCreateRootFromDOMContainer(
490
container: DOMContainer,
491
forceHydrate: boolean,
495
-): Root {
492
+): _ReactSyncRoot {
493
const shouldHydrate =
494
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
495
// First clear any existing content.
@@ -529,7 +526,9 @@ function legacyCreateRootFromDOMContainer(
526
);
527
}
528
}
532
- return new ReactRoot(container, LegacyRoot, shouldHydrate);
529
+
530
+ // Legacy roots are not batched.
531
+ return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
532
}
533
534
function legacyRenderSubtreeIntoContainer(
@@ -541,56 +540,44 @@ function legacyRenderSubtreeIntoContainer(
540
) {
541
if (__DEV__) {
542
topLevelUpdateWarnings(container);
543
+ warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
544
}
545
546
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
547
// member of intersection type." Whyyyyyy.
548
- let root: Root = (container._reactRootContainer: any);
548
+ let root: _ReactSyncRoot = (container._reactRootContainer: any);
549
+ let fiberRoot;
550
if (!root) {
551
// Initial mount
552
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
553
container,
554
forceHydrate,
555
);
556
+ fiberRoot = root._internalRoot;
557
if (typeof callback === 'function') {
558
const originalCallback = callback;
559
callback = function() {
558
- const instance = getPublicRootInstance(root._internalRoot);
560
+ const instance = getPublicRootInstance(fiberRoot);
561
originalCallback.call(instance);
562
};
563
}
564
// Initial mount should not be batched.
565
unbatchedUpdates(() => {
564
- if (parentComponent != null) {
565
- root.legacy_renderSubtreeIntoContainer(
566
- parentComponent,
567
- children,
568
- callback,
569
- );
570
- } else {
571
- root.render(children, callback);
572
- }
566
+ updateContainer(children, fiberRoot, parentComponent, callback);
567
});
568
} else {
569
+ fiberRoot = root._internalRoot;
570
if (typeof callback === 'function') {
571
const originalCallback = callback;
572
callback = function() {
578
- const instance = getPublicRootInstance(root._internalRoot);
573
+ const instance = getPublicRootInstance(fiberRoot);
574
originalCallback.call(instance);
575
};
576
}
577
// Update
583
- if (parentComponent != null) {
584
- root.legacy_renderSubtreeIntoContainer(
585
- parentComponent,
586
- children,
587
- callback,
588
- );
589
- } else {
590
- root.render(children, callback);
591
- }
578
+ updateContainer(children, fiberRoot, parentComponent, callback);
579
}
593
- return getPublicRootInstance(root._internalRoot);
580
+ return getPublicRootInstance(fiberRoot);
581
}
582
583
function createPortal(
@@ -800,6 +787,7 @@ const ReactDOM: Object = {
787
flushSync: flushSync,
788
789
unstable_createRoot: createRoot,
790
+ unstable_createSyncRoot: createSyncRoot,
791
unstable_flushControlled: flushControlled,
792
793
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
@@ -826,7 +814,10 @@ type RootOptions = {
814
hydrate?: boolean,
815
};
816
829
-function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
817
+function createRoot(
818
+ container: DOMContainer,
819
+ options?: RootOptions,
820
+): _ReactRoot {
821
const functionName = enableStableConcurrentModeAPIs
822
? 'createRoot'
823
: 'unstable_createRoot';
@@ -835,6 +826,29 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
826
'%s(...): Target container is not a DOM element.',
827
functionName,
828
);
829
+ warnIfReactDOMContainerInDEV(container);
830
+ const hydrate = options != null && options.hydrate === true;
831
+ return new ReactRoot(container, hydrate);
832
+}
833
+
834
+function createSyncRoot(
835
+ container: DOMContainer,
836
+ options?: RootOptions,
837
+): _ReactSyncRoot {
838
+ const functionName = enableStableConcurrentModeAPIs
839
+ ? 'createRoot'
840
+ : 'unstable_createRoot';
841
+ invariant(
842
+ isValidContainer(container),
843
+ '%s(...): Target container is not a DOM element.',
844
+ functionName,
845
+ );
846
+ warnIfReactDOMContainerInDEV(container);
847
+ const hydrate = options != null && options.hydrate === true;
848
+ return new ReactSyncRoot(container, BatchedRoot, hydrate);
849
+}
850
+
851
+function warnIfReactDOMContainerInDEV(container) {
852
if (__DEV__) {
853
warningWithoutStack(
854
!container._reactRootContainer,
@@ -844,13 +858,11 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
858
);
859
container._reactHasBeenPassedToCreateRootDEV = true;
860
}
847
- const hydrate = options != null && options.hydrate === true;
848
- return new ReactRoot(container, ConcurrentRoot, hydrate);
861
}
862
863
if (enableStableConcurrentModeAPIs) {
864
ReactDOM.createRoot = createRoot;
853
- ReactDOM.unstable_createRoot = undefined;
865
+ ReactDOM.createSyncRoot = createSyncRoot;
866
}
867
868
const foundDevTools = injectIntoDevTools({
packages/react-dom/src/fire/ReactFire.js
+72
-60
@@ -59,7 +59,7 @@ import {
59
accumulateTwoPhaseDispatches,
60
accumulateDirectDispatches,
61
} from 'events/EventPropagators';
62
-import {LegacyRoot, ConcurrentRoot} from 'shared/ReactRootTags';
62
+import {LegacyRoot, ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
63
import {has as hasInstance} from 'shared/ReactInstanceMap';
64
import ReactVersion from 'shared/ReactVersion';
65
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -165,11 +165,11 @@ setRestoreImplementation(restoreControlledState);
165
166
export type DOMContainer =
167
| (Element & {
168
- _reactRootContainer: ?Root,
168
+ _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
169
_reactHasBeenPassedToCreateRootDEV: ?boolean,
170
})
171
| (Document & {
172
- _reactRootContainer: ?Root,
172
+ _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
173
_reactHasBeenPassedToCreateRootDEV: ?boolean,
174
});
175
@@ -181,7 +181,7 @@ type Batch = FiberRootBatch & {
181
// The ReactRoot constructor is hoisted but the prototype methods are not. If
182
// we move ReactRoot to be above ReactBatch, the inverse error occurs.
183
// $FlowFixMe Hoisting issue.
184
- _root: Root,
184
+ _root: _ReactRoot | _ReactSyncRoot,
185
_hasChildren: boolean,
186
_children: ReactNodeList,
187
@@ -189,20 +189,18 @@ type Batch = FiberRootBatch & {
189
_didComplete: boolean,
190
};
191
192
-type Root = {
192
+type _ReactSyncRoot = {
193
render(children: ReactNodeList, callback: ?() => mixed): Work,
194
unmount(callback: ?() => mixed): Work,
195
- legacy_renderSubtreeIntoContainer(
196
- parentComponent: ?React$Component<any, any>,
197
- children: ReactNodeList,
198
- callback: ?() => mixed,
199
- ): Work,
200
- createBatch(): Batch,
195
196
_internalRoot: FiberRoot,
197
};
198
205
-function ReactBatch(root: ReactRoot) {
199
+type _ReactRoot = _ReactSyncRoot & {
200
+ createBatch(): Batch,
201
+};
202
+
203
+function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
204
const expirationTime = computeUniqueAsyncExpiration();
205
this._expirationTime = expirationTime;
206
this._root = root;
@@ -369,11 +367,22 @@ ReactWork.prototype._onCommit = function(): void {
367
}
368
};
369
372
-function ReactRoot(container: DOMContainer, tag: RootTag, hydrate: boolean) {
370
+function ReactSyncRoot(
371
+ container: DOMContainer,
372
+ tag: RootTag,
373
+ hydrate: boolean,
374
+) {
375
+ // Tag is either LegacyRoot or Concurrent Root
376
const root = createContainer(container, tag, hydrate);
377
this._internalRoot = root;
378
}
376
-ReactRoot.prototype.render = function(
379
+
380
+function ReactRoot(container: DOMContainer, hydrate: boolean) {
381
+ const root = createContainer(container, ConcurrentRoot, hydrate);
382
+ this._internalRoot = root;
383
+}
384
+
385
+ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
386
children: ReactNodeList,
387
callback: ?() => mixed,
388
): Work {
@@ -389,22 +398,8 @@ ReactRoot.prototype.render = function(
398
updateContainer(children, root, null, work._onCommit);
399
return work;
400
};
392
-ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work {
393
- const root = this._internalRoot;
394
- const work = new ReactWork();
395
- callback = callback === undefined ? null : callback;
396
- if (__DEV__) {
397
- warnOnInvalidCallback(callback, 'render');
398
- }
399
- if (callback !== null) {
400
- work.then(callback);
401
- }
402
- updateContainer(null, root, null, work._onCommit);
403
- return work;
404
-};
405
-ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
406
- parentComponent: ?React$Component<any, any>,
407
- children: ReactNodeList,
401
+
402
+ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
403
callback: ?() => mixed,
404
): Work {
405
const root = this._internalRoot;
@@ -416,9 +411,11 @@ ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
411
if (callback !== null) {
412
work.then(callback);
413
}
419
- updateContainer(children, root, parentComponent, work._onCommit);
414
+ updateContainer(null, root, null, work._onCommit);
415
return work;
416
};
417
+
418
+// Sync roots cannot create batches. Only concurrent ones.
419
ReactRoot.prototype.createBatch = function(): Batch {
420
const batch = new ReactBatch(this);
421
const expirationTime = batch._expirationTime;
@@ -498,7 +495,7 @@ let warnedAboutHydrateAPI = false;
495
function legacyCreateRootFromDOMContainer(
496
container: DOMContainer,
497
forceHydrate: boolean,
501
-): Root {
498
+): _ReactSyncRoot {
499
const shouldHydrate =
500
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
501
// First clear any existing content.
@@ -535,7 +532,9 @@ function legacyCreateRootFromDOMContainer(
532
);
533
}
534
}
538
- return new ReactRoot(container, LegacyRoot, shouldHydrate);
535
+
536
+ // Legacy roots are not batched.
537
+ return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
538
}
539
540
function legacyRenderSubtreeIntoContainer(
@@ -547,56 +546,44 @@ function legacyRenderSubtreeIntoContainer(
546
) {
547
if (__DEV__) {
548
topLevelUpdateWarnings(container);
549
+ warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
550
}
551
552
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
553
// member of intersection type." Whyyyyyy.
554
- let root: Root = (container._reactRootContainer: any);
554
+ let root: _ReactSyncRoot = (container._reactRootContainer: any);
555
+ let fiberRoot;
556
if (!root) {
557
// Initial mount
558
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
559
container,
560
forceHydrate,
561
);
562
+ fiberRoot = root._internalRoot;
563
if (typeof callback === 'function') {
564
const originalCallback = callback;
565
callback = function() {
564
- const instance = getPublicRootInstance(root._internalRoot);
566
+ const instance = getPublicRootInstance(fiberRoot);
567
originalCallback.call(instance);
568
};
569
}
570
// Initial mount should not be batched.
571
unbatchedUpdates(() => {
570
- if (parentComponent != null) {
571
- root.legacy_renderSubtreeIntoContainer(
572
- parentComponent,
573
- children,
574
- callback,
575
- );
576
- } else {
577
- root.render(children, callback);
578
- }
572
+ updateContainer(children, fiberRoot, parentComponent, callback);
573
});
574
} else {
575
+ fiberRoot = root._internalRoot;
576
if (typeof callback === 'function') {
577
const originalCallback = callback;
578
callback = function() {
584
- const instance = getPublicRootInstance(root._internalRoot);
579
+ const instance = getPublicRootInstance(fiberRoot);
580
originalCallback.call(instance);
581
};
582
}
583
// Update
589
- if (parentComponent != null) {
590
- root.legacy_renderSubtreeIntoContainer(
591
- parentComponent,
592
- children,
593
- callback,
594
- );
595
- } else {
596
- root.render(children, callback);
597
- }
584
+ updateContainer(children, fiberRoot, parentComponent, callback);
585
}
599
- return getPublicRootInstance(root._internalRoot);
586
+ return getPublicRootInstance(fiberRoot);
587
}
588
589
function createPortal(
@@ -806,11 +793,12 @@ const ReactDOM: Object = {
793
flushSync: flushSync,
794
795
unstable_createRoot: createRoot,
796
+ unstable_createSyncRoot: createSyncRoot,
797
unstable_flushControlled: flushControlled,
798
799
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
800
// Keep in sync with ReactDOMUnstableNativeDependencies.js
813
- // and ReactTestUtils.js. This is an array for better minification.
801
+ // ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
802
Events: [
803
getInstanceFromNode,
804
getNodeFromInstance,
@@ -832,7 +820,10 @@ type RootOptions = {
820
hydrate?: boolean,
821
};
822
835
-function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
823
+function createRoot(
824
+ container: DOMContainer,
825
+ options?: RootOptions,
826
+): _ReactRoot {
827
const functionName = enableStableConcurrentModeAPIs
828
? 'createRoot'
829
: 'unstable_createRoot';
@@ -841,6 +832,29 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
832
'%s(...): Target container is not a DOM element.',
833
functionName,
834
);
835
+ warnIfReactDOMContainerInDEV(container);
836
+ const hydrate = options != null && options.hydrate === true;
837
+ return new ReactRoot(container, hydrate);
838
+}
839
+
840
+function createSyncRoot(
841
+ container: DOMContainer,
842
+ options?: RootOptions,
843
+): _ReactSyncRoot {
844
+ const functionName = enableStableConcurrentModeAPIs
845
+ ? 'createRoot'
846
+ : 'unstable_createRoot';
847
+ invariant(
848
+ isValidContainer(container),
849
+ '%s(...): Target container is not a DOM element.',
850
+ functionName,
851
+ );
852
+ warnIfReactDOMContainerInDEV(container);
853
+ const hydrate = options != null && options.hydrate === true;
854
+ return new ReactSyncRoot(container, BatchedRoot, hydrate);
855
+}
856
+
857
+function warnIfReactDOMContainerInDEV(container) {
858
if (__DEV__) {
859
warningWithoutStack(
860
!container._reactRootContainer,
@@ -850,13 +864,11 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot {
864
);
865
container._reactHasBeenPassedToCreateRootDEV = true;
866
}
853
- const hydrate = options != null && options.hydrate === true;
854
- return new ReactRoot(container, ConcurrentRoot, hydrate);
867
}
868
869
if (enableStableConcurrentModeAPIs) {
870
ReactDOM.createRoot = createRoot;
859
- ReactDOM.unstable_createRoot = undefined;
871
+ ReactDOM.createSyncRoot = createSyncRoot;
872
}
873
874
const foundDevTools = injectIntoDevTools({