Remove dormant createBatch experiment (#17035)
* Remove dormant createBatch experiment In a hybrid React app with multiple roots, `createBatch` is used to coordinate an update to a root with its imperative container. We've pivoted away from multi-root, hybrid React apps for now to focus on single root apps. This PR removes the API from the codebase. It's possible we'll add back some version of this feature in the future. * Remove unused export
Andrew Clark committed
Oct 7, 2019 at 14:15 UTC
71d012ecd07baef6f53d02bebd720794f75266ca
10 files changed
+150
-815
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
-223
@@ -43,35 +43,6 @@ describe('ReactDOMRoot', () => {
43
expect(container.textContent).toEqual('');
44
});
45
46
- it('`root.render` returns a thenable work object', () => {
47
- const root = ReactDOM.unstable_createRoot(container);
48
- const work = root.render('Hi');
49
- let ops = [];
50
- work.then(() => {
51
- ops.push('inside callback: ' + container.textContent);
52
- });
53
- ops.push('before committing: ' + container.textContent);
54
- Scheduler.unstable_flushAll();
55
- ops.push('after committing: ' + container.textContent);
56
- expect(ops).toEqual([
57
- 'before committing: ',
58
- // `then` callback should fire during commit phase
59
- 'inside callback: Hi',
60
- 'after committing: Hi',
61
- ]);
62
- });
63
-
64
- it('resolves `work.then` callback synchronously if the work already committed', () => {
65
- const root = ReactDOM.unstable_createRoot(container);
66
- const work = root.render('Hi');
67
- Scheduler.unstable_flushAll();
68
- let ops = [];
69
- work.then(() => {
70
- ops.push('inside callback');
71
- });
72
- expect(ops).toEqual(['inside callback']);
73
- });
74
-
46
it('supports hydration', async () => {
47
const markup = await new Promise(resolve =>
48
resolve(
@@ -129,200 +100,6 @@ describe('ReactDOMRoot', () => {
100
expect(container.textContent).toEqual('abdc');
101
});
102
132
- it('can defer a commit by batching it', () => {
133
- const root = ReactDOM.unstable_createRoot(container);
134
- const batch = root.createBatch();
135
- batch.render(<div>Hi</div>);
136
- // Hasn't committed yet
137
- expect(container.textContent).toEqual('');
138
- // Commit
139
- batch.commit();
140
- expect(container.textContent).toEqual('Hi');
141
- });
142
-
143
- it('applies setState in componentDidMount synchronously in a batch', done => {
144
- class App extends React.Component {
145
- state = {mounted: false};
146
- componentDidMount() {
147
- this.setState({
148
- mounted: true,
149
- });
150
- }
151
- render() {
152
- return this.state.mounted ? 'Hi' : 'Bye';
153
- }
154
- }
155
-
156
- const root = ReactDOM.unstable_createRoot(container);
157
- const batch = root.createBatch();
158
- batch.render(<App />);
159
-
160
- Scheduler.unstable_flushAll();
161
-
162
- // Hasn't updated yet
163
- expect(container.textContent).toEqual('');
164
-
165
- let ops = [];
166
- batch.then(() => {
167
- // Still hasn't updated
168
- ops.push(container.textContent);
169
-
170
- // Should synchronously commit
171
- batch.commit();
172
- ops.push(container.textContent);
173
-
174
- expect(ops).toEqual(['', 'Hi']);
175
- done();
176
- });
177
- });
178
-
179
- it('does not restart a completed batch when committing if there were no intervening updates', () => {
180
- let ops = [];
181
- function Foo(props) {
182
- ops.push('Foo');
183
- return props.children;
184
- }
185
- const root = ReactDOM.unstable_createRoot(container);
186
- const batch = root.createBatch();
187
- batch.render(<Foo>Hi</Foo>);
188
- // Flush all async work.
189
- Scheduler.unstable_flushAll();
190
- // Root should complete without committing.
191
- expect(ops).toEqual(['Foo']);
192
- expect(container.textContent).toEqual('');
193
-
194
- ops = [];
195
-
196
- // Commit. Shouldn't re-render Foo.
197
- batch.commit();
198
- expect(ops).toEqual([]);
199
- expect(container.textContent).toEqual('Hi');
200
- });
201
-
202
- it('can wait for a batch to finish', () => {
203
- const root = ReactDOM.unstable_createRoot(container);
204
- const batch = root.createBatch();
205
- batch.render('Foo');
206
-
207
- Scheduler.unstable_flushAll();
208
-
209
- // Hasn't updated yet
210
- expect(container.textContent).toEqual('');
211
-
212
- let ops = [];
213
- batch.then(() => {
214
- // Still hasn't updated
215
- ops.push(container.textContent);
216
- // Should synchronously commit
217
- batch.commit();
218
- ops.push(container.textContent);
219
- });
220
-
221
- expect(ops).toEqual(['', 'Foo']);
222
- });
223
-
224
- it('`batch.render` returns a thenable work object', () => {
225
- const root = ReactDOM.unstable_createRoot(container);
226
- const batch = root.createBatch();
227
- const work = batch.render('Hi');
228
- let ops = [];
229
- work.then(() => {
230
- ops.push('inside callback: ' + container.textContent);
231
- });
232
- ops.push('before committing: ' + container.textContent);
233
- batch.commit();
234
- ops.push('after committing: ' + container.textContent);
235
- expect(ops).toEqual([
236
- 'before committing: ',
237
- // `then` callback should fire during commit phase
238
- 'inside callback: Hi',
239
- 'after committing: Hi',
240
- ]);
241
- });
242
-
243
- it('can commit an empty batch', () => {
244
- const root = ReactDOM.unstable_createRoot(container);
245
- root.render(1);
246
-
247
- Scheduler.unstable_advanceTime(2000);
248
- // This batch has a later expiration time than the earlier update.
249
- const batch = root.createBatch();
250
-
251
- // This should not flush the earlier update.
252
- batch.commit();
253
- expect(container.textContent).toEqual('');
254
-
255
- Scheduler.unstable_flushAll();
256
- expect(container.textContent).toEqual('1');
257
- });
258
-
259
- it('two batches created simultaneously are committed separately', () => {
260
- // (In other words, they have distinct expiration times)
261
- const root = ReactDOM.unstable_createRoot(container);
262
- const batch1 = root.createBatch();
263
- batch1.render(1);
264
- const batch2 = root.createBatch();
265
- batch2.render(2);
266
-
267
- expect(container.textContent).toEqual('');
268
-
269
- batch1.commit();
270
- expect(container.textContent).toEqual('1');
271
-
272
- batch2.commit();
273
- expect(container.textContent).toEqual('2');
274
- });
275
-
276
- it('commits an earlier batch without committing a later batch', () => {
277
- const root = ReactDOM.unstable_createRoot(container);
278
- const batch1 = root.createBatch();
279
- batch1.render(1);
280
-
281
- // This batch has a later expiration time
282
- Scheduler.unstable_advanceTime(2000);
283
- const batch2 = root.createBatch();
284
- batch2.render(2);
285
-
286
- expect(container.textContent).toEqual('');
287
-
288
- batch1.commit();
289
- expect(container.textContent).toEqual('1');
290
-
291
- batch2.commit();
292
- expect(container.textContent).toEqual('2');
293
- });
294
-
295
- it('commits a later batch without committing an earlier batch', () => {
296
- const root = ReactDOM.unstable_createRoot(container);
297
- const batch1 = root.createBatch();
298
- batch1.render(1);
299
-
300
- // This batch has a later expiration time
301
- Scheduler.unstable_advanceTime(2000);
302
- const batch2 = root.createBatch();
303
- batch2.render(2);
304
-
305
- expect(container.textContent).toEqual('');
306
-
307
- batch2.commit();
308
- expect(container.textContent).toEqual('2');
309
-
310
- batch1.commit();
311
- Scheduler.unstable_flushAll();
312
- expect(container.textContent).toEqual('1');
313
- });
314
-
315
- it('handles fatal errors triggered by batch.commit()', () => {
316
- const root = ReactDOM.unstable_createRoot(container);
317
- const batch = root.createBatch();
318
- const InvalidType = undefined;
319
- expect(() => batch.render(<InvalidType />)).toWarnDev(
320
- ['React.createElement: type is invalid'],
321
- {withoutStack: true},
322
- );
323
- expect(() => batch.commit()).toThrow('Element type is invalid');
324
- });
325
-
103
it('throws a good message on invalid containers', () => {
104
expect(() => {
105
ReactDOM.unstable_createRoot(<div>Hi</div>);
packages/react-dom/src/client/ReactDOM.js
+13
-247
@@ -11,19 +11,13 @@ import type {ReactNodeList} from 'shared/ReactTypes';
11
import type {RootTag} from 'shared/ReactRootTags';
12
// TODO: This type is shared between the reconciler and ReactDOM, but will
13
// eventually be lifted out to the renderer.
14
-import type {
15
- FiberRoot,
16
- Batch as FiberRootBatch,
17
-} from 'react-reconciler/src/ReactFiberRoot';
14
+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
15
16
import '../shared/checkReact';
17
import './ReactDOMClientInjection';
18
19
import {
23
- computeUniqueAsyncExpiration,
20
findHostInstanceWithNoPortals,
25
- updateContainerAtExpirationTime,
26
- flushRoot,
21
createContainer,
22
updateContainer,
23
batchedEventUpdates,
@@ -179,209 +173,21 @@ setRestoreImplementation(restoreControlledState);
173
174
export type DOMContainer =
175
| (Element & {
182
- _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
176
+ _reactRootContainer: ?_ReactRoot,
177
_reactHasBeenPassedToCreateRootDEV: ?boolean,
178
})
179
| (Document & {
186
- _reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
180
+ _reactRootContainer: ?_ReactRoot,
181
_reactHasBeenPassedToCreateRootDEV: ?boolean,
182
});
183
190
-type Batch = FiberRootBatch & {
191
- render(children: ReactNodeList): Work,
192
- then(onComplete: () => mixed): void,
193
- commit(): void,
194
-
195
- // The ReactRoot constructor is hoisted but the prototype methods are not. If
196
- // we move ReactRoot to be above ReactBatch, the inverse error occurs.
197
- // $FlowFixMe Hoisting issue.
198
- _root: _ReactRoot | _ReactSyncRoot,
199
- _hasChildren: boolean,
200
- _children: ReactNodeList,
201
-
202
- _callbacks: Array<() => mixed> | null,
203
- _didComplete: boolean,
204
-};
205
-
206
-type _ReactSyncRoot = {
207
- render(children: ReactNodeList, callback: ?() => mixed): Work,
208
- unmount(callback: ?() => mixed): Work,
184
+type _ReactRoot = {
185
+ render(children: ReactNodeList, callback: ?() => mixed): void,
186
+ unmount(callback: ?() => mixed): void,
187
188
_internalRoot: FiberRoot,
189
};
190
213
-type _ReactRoot = _ReactSyncRoot & {
214
- createBatch(): Batch,
215
-};
216
-
217
-function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
218
- const expirationTime = computeUniqueAsyncExpiration();
219
- this._expirationTime = expirationTime;
220
- this._root = root;
221
- this._next = null;
222
- this._callbacks = null;
223
- this._didComplete = false;
224
- this._hasChildren = false;
225
- this._children = null;
226
- this._defer = true;
227
-}
228
-ReactBatch.prototype.render = function(children: ReactNodeList) {
229
- invariant(
230
- this._defer,
231
- 'batch.render: Cannot render a batch that already committed.',
232
- );
233
- this._hasChildren = true;
234
- this._children = children;
235
- const internalRoot = this._root._internalRoot;
236
- const expirationTime = this._expirationTime;
237
- const work = new ReactWork();
238
- updateContainerAtExpirationTime(
239
- children,
240
- internalRoot,
241
- null,
242
- expirationTime,
243
- null,
244
- work._onCommit,
245
- );
246
- return work;
247
-};
248
-ReactBatch.prototype.then = function(onComplete: () => mixed) {
249
- if (this._didComplete) {
250
- onComplete();
251
- return;
252
- }
253
- let callbacks = this._callbacks;
254
- if (callbacks === null) {
255
- callbacks = this._callbacks = [];
256
- }
257
- callbacks.push(onComplete);
258
-};
259
-ReactBatch.prototype.commit = function() {
260
- const internalRoot = this._root._internalRoot;
261
- let firstBatch = internalRoot.firstBatch;
262
- invariant(
263
- this._defer && firstBatch !== null,
264
- 'batch.commit: Cannot commit a batch multiple times.',
265
- );
266
-
267
- if (!this._hasChildren) {
268
- // This batch is empty. Return.
269
- this._next = null;
270
- this._defer = false;
271
- return;
272
- }
273
-
274
- let expirationTime = this._expirationTime;
275
-
276
- // Ensure this is the first batch in the list.
277
- if (firstBatch !== this) {
278
- // This batch is not the earliest batch. We need to move it to the front.
279
- // Update its expiration time to be the expiration time of the earliest
280
- // batch, so that we can flush it without flushing the other batches.
281
- if (this._hasChildren) {
282
- expirationTime = this._expirationTime = firstBatch._expirationTime;
283
- // Rendering this batch again ensures its children will be the final state
284
- // when we flush (updates are processed in insertion order: last
285
- // update wins).
286
- // TODO: This forces a restart. Should we print a warning?
287
- this.render(this._children);
288
- }
289
-
290
- // Remove the batch from the list.
291
- let previous = null;
292
- let batch = firstBatch;
293
- while (batch !== this) {
294
- previous = batch;
295
- batch = batch._next;
296
- }
297
- invariant(
298
- previous !== null,
299
- 'batch.commit: Cannot commit a batch multiple times.',
300
- );
301
- previous._next = batch._next;
302
-
303
- // Add it to the front.
304
- this._next = firstBatch;
305
- firstBatch = internalRoot.firstBatch = this;
306
- }
307
-
308
- // Synchronously flush all the work up to this batch's expiration time.
309
- this._defer = false;
310
- flushRoot(internalRoot, expirationTime);
311
-
312
- // Pop the batch from the list.
313
- const next = this._next;
314
- this._next = null;
315
- firstBatch = internalRoot.firstBatch = next;
316
-
317
- // Append the next earliest batch's children to the update queue.
318
- if (firstBatch !== null && firstBatch._hasChildren) {
319
- firstBatch.render(firstBatch._children);
320
- }
321
-};
322
-ReactBatch.prototype._onComplete = function() {
323
- if (this._didComplete) {
324
- return;
325
- }
326
- this._didComplete = true;
327
- const callbacks = this._callbacks;
328
- if (callbacks === null) {
329
- return;
330
- }
331
- // TODO: Error handling.
332
- for (let i = 0; i < callbacks.length; i++) {
333
- const callback = callbacks[i];
334
- callback();
335
- }
336
-};
337
-
338
-type Work = {
339
- then(onCommit: () => mixed): void,
340
- _onCommit: () => void,
341
- _callbacks: Array<() => mixed> | null,
342
- _didCommit: boolean,
343
-};
344
-
345
-function ReactWork() {
346
- this._callbacks = null;
347
- this._didCommit = false;
348
- // TODO: Avoid need to bind by replacing callbacks in the update queue with
349
- // list of Work objects.
350
- this._onCommit = this._onCommit.bind(this);
351
-}
352
-ReactWork.prototype.then = function(onCommit: () => mixed): void {
353
- if (this._didCommit) {
354
- onCommit();
355
- return;
356
- }
357
- let callbacks = this._callbacks;
358
- if (callbacks === null) {
359
- callbacks = this._callbacks = [];
360
- }
361
- callbacks.push(onCommit);
362
-};
363
-ReactWork.prototype._onCommit = function(): void {
364
- if (this._didCommit) {
365
- return;
366
- }
367
- this._didCommit = true;
368
- const callbacks = this._callbacks;
369
- if (callbacks === null) {
370
- return;
371
- }
372
- // TODO: Error handling.
373
- for (let i = 0; i < callbacks.length; i++) {
374
- const callback = callbacks[i];
375
- invariant(
376
- typeof callback === 'function',
377
- 'Invalid argument passed as callback. Expected a function. Instead ' +
378
- 'received: %s',
379
- callback,
380
- );
381
- callback();
382
- }
383
-};
384
-
191
function createRootImpl(
192
container: DOMContainer,
193
tag: RootTag,
@@ -418,64 +224,24 @@ function ReactRoot(container: DOMContainer, options: void | RootOptions) {
224
ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
225
children: ReactNodeList,
226
callback: ?() => mixed,
421
-): Work {
227
+): void {
228
const root = this._internalRoot;
423
- const work = new ReactWork();
229
callback = callback === undefined ? null : callback;
230
if (__DEV__) {
231
warnOnInvalidCallback(callback, 'render');
232
}
428
- if (callback !== null) {
429
- work.then(callback);
430
- }
431
- updateContainer(children, root, null, work._onCommit);
432
- return work;
233
+ updateContainer(children, root, null, callback);
234
};
235
236
ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
237
callback: ?() => mixed,
437
-): Work {
238
+): void {
239
const root = this._internalRoot;
439
- const work = new ReactWork();
240
callback = callback === undefined ? null : callback;
241
if (__DEV__) {
242
warnOnInvalidCallback(callback, 'render');
243
}
444
- if (callback !== null) {
445
- work.then(callback);
446
- }
447
- updateContainer(null, root, null, work._onCommit);
448
- return work;
449
-};
450
-
451
-// Sync roots cannot create batches. Only concurrent ones.
452
-ReactRoot.prototype.createBatch = function(): Batch {
453
- const batch = new ReactBatch(this);
454
- const expirationTime = batch._expirationTime;
455
-
456
- const internalRoot = this._internalRoot;
457
- const firstBatch = internalRoot.firstBatch;
458
- if (firstBatch === null) {
459
- internalRoot.firstBatch = batch;
460
- batch._next = null;
461
- } else {
462
- // Insert sorted by expiration time then insertion order
463
- let insertAfter = null;
464
- let insertBefore = firstBatch;
465
- while (
466
- insertBefore !== null &&
467
- insertBefore._expirationTime >= expirationTime
468
- ) {
469
- insertAfter = insertBefore;
470
- insertBefore = insertBefore._next;
471
- }
472
- batch._next = insertBefore;
473
- if (insertAfter !== null) {
474
- insertAfter._next = batch;
475
- }
476
- }
477
-
478
- return batch;
244
+ updateContainer(null, root, null, callback);
245
};
246
247
/**
@@ -529,7 +295,7 @@ let warnedAboutHydrateAPI = false;
295
function legacyCreateRootFromDOMContainer(
296
container: DOMContainer,
297
forceHydrate: boolean,
532
-): _ReactSyncRoot {
298
+): _ReactRoot {
299
const shouldHydrate =
300
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
301
// First clear any existing content.
@@ -593,7 +359,7 @@ function legacyRenderSubtreeIntoContainer(
359
360
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
361
// member of intersection type." Whyyyyyy.
596
- let root: _ReactSyncRoot = (container._reactRootContainer: any);
362
+ let root: _ReactRoot = (container._reactRootContainer: any);
363
let fiberRoot;
364
if (!root) {
365
// Initial mount
@@ -899,7 +665,7 @@ function createRoot(
665
function createSyncRoot(
666
container: DOMContainer,
667
options?: RootOptions,
902
-): _ReactSyncRoot {
668
+): _ReactRoot {
669
const functionName = enableStableConcurrentModeAPIs
670
? 'createRoot'
671
: 'unstable_createRoot';
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+7
-11
@@ -857,7 +857,7 @@ describe('DOMEventResponderSystem', () => {
857
858
function Test({counter}) {
859
const listener = React.unstable_useResponder(TestResponder, {counter});
860
-
860
+ Scheduler.unstable_yieldValue('Test');
861
return (
862
<button listeners={listener} ref={ref}>
863
Press me
@@ -866,11 +866,8 @@ describe('DOMEventResponderSystem', () => {
866
}
867
868
let root = ReactDOM.unstable_createRoot(container);
869
- let batch = root.createBatch();
870
- batch.render(<Test counter={0} />);
871
- Scheduler.unstable_flushAll();
872
- jest.runAllTimers();
873
- batch.commit();
869
+ root.render(<Test counter={0} />);
870
+ expect(Scheduler).toFlushAndYield(['Test']);
871
872
// Click the button
873
dispatchClickEvent(ref.current);
@@ -880,10 +877,9 @@ describe('DOMEventResponderSystem', () => {
877
log.length = 0;
878
879
// Increase counter
883
- batch = root.createBatch();
884
- batch.render(<Test counter={1} />);
885
- Scheduler.unstable_flushAll();
886
- jest.runAllTimers();
880
+ root.render(<Test counter={1} />);
881
+ // Yield before committing
882
+ expect(Scheduler).toFlushAndYieldThrough(['Test']);
883
884
// Click the button again
885
dispatchClickEvent(ref.current);
@@ -893,7 +889,7 @@ describe('DOMEventResponderSystem', () => {
889
log.length = 0;
890
891
// Commit
896
- batch.commit();
892
+ expect(Scheduler).toFlushAndYield([]);
893
dispatchClickEvent(ref.current);
894
expect(log).toEqual([{counter: 1}]);
895
});
packages/react-noop-renderer/src/createReactNoop.js
-26
@@ -22,7 +22,6 @@ import type {RootTag} from 'shared/ReactRootTags';
22
23
import * as Scheduler from 'scheduler/unstable_mock';
24
import {createPortal} from 'shared/ReactPortal';
25
-import expect from 'expect';
25
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
26
import enqueueTask from 'shared/enqueueTask';
27
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -1198,31 +1197,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1197
console.log(...bufferedLog);
1198
},
1199
1201
- flushWithoutCommitting(
1202
- expectedFlush: Array<mixed>,
1203
- rootID: string = DEFAULT_ROOT_ID,
1204
- ) {
1205
- const root: any = roots.get(rootID);
1206
- const expiration = NoopRenderer.computeUniqueAsyncExpiration();
1207
- const batch = {
1208
- _defer: true,
1209
- _expirationTime: expiration,
1210
- _onComplete: () => {
1211
- root.firstBatch = null;
1212
- },
1213
- _next: null,
1214
- };
1215
- root.firstBatch = batch;
1216
- Scheduler.unstable_flushAllWithoutAsserting();
1217
- const actual = Scheduler.unstable_clearYields();
1218
- expect(actual).toEqual(expectedFlush);
1219
- return (expectedCommit: Array<mixed>) => {
1220
- batch._defer = false;
1221
- NoopRenderer.flushRoot(root, expiration);
1222
- expect(Scheduler.unstable_clearYields()).toEqual(expectedCommit);
1223
- };
1224
- },
1225
-
1200
getRoot(rootID: string = DEFAULT_ROOT_ID) {
1201
return roots.get(rootID);
1202
},
packages/react-reconciler/src/ReactFiberHotReloading.js
+4
-2
@@ -18,7 +18,7 @@ import {
18
scheduleWork,
19
flushPassiveEffects,
20
} from './ReactFiberWorkLoop';
21
-import {updateContainerAtExpirationTime} from './ReactFiberReconciler';
21
+import {updateContainer, syncUpdates} from './ReactFiberReconciler';
22
import {emptyContextObject} from './ReactFiberContext';
23
import {Sync} from './ReactFiberExpirationTime';
24
import {
@@ -258,7 +258,9 @@ export let scheduleRoot: ScheduleRoot = (
258
return;
259
}
260
flushPassiveEffects();
261
- updateContainerAtExpirationTime(element, root, null, Sync, null);
261
+ syncUpdates(() => {
262
+ updateContainer(element, root, null, null);
263
+ });
264
}
265
};
266
packages/react-reconciler/src/ReactFiberReconciler.js
+58
-97
@@ -19,7 +19,6 @@ import type {
19
import {FundamentalComponent} from 'shared/ReactWorkTags';
20
import type {ReactNodeList} from 'shared/ReactTypes';
21
import type {ExpirationTime} from './ReactFiberExpirationTime';
22
-import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
22
import type {
23
SuspenseHydrationCallbacks,
24
SuspenseState,
@@ -51,7 +50,6 @@ import {
50
import {createFiberRoot} from './ReactFiberRoot';
51
import {injectInternals} from './ReactFiberDevToolsHook';
52
import {
54
- computeUniqueAsyncExpiration,
53
requestCurrentTime,
54
computeExpirationForFiber,
55
scheduleWork,
@@ -138,92 +136,6 @@ function getContextForSubtree(
136
return parentContext;
137
}
138
141
-function scheduleRootUpdate(
142
- current: Fiber,
143
- element: ReactNodeList,
144
- expirationTime: ExpirationTime,
145
- suspenseConfig: null | SuspenseConfig,
146
- callback: ?Function,
147
-) {
148
- if (__DEV__) {
149
- if (
150
- ReactCurrentFiberPhase === 'render' &&
151
- ReactCurrentFiberCurrent !== null &&
152
- !didWarnAboutNestedUpdates
153
- ) {
154
- didWarnAboutNestedUpdates = true;
155
- warningWithoutStack(
156
- false,
157
- 'Render methods should be a pure function of props and state; ' +
158
- 'triggering nested component updates from render is not allowed. ' +
159
- 'If necessary, trigger nested updates in componentDidUpdate.\n\n' +
160
- 'Check the render method of %s.',
161
- getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown',
162
- );
163
- }
164
- }
165
-
166
- const update = createUpdate(expirationTime, suspenseConfig);
167
- // Caution: React DevTools currently depends on this property
168
- // being called "element".
169
- update.payload = {element};
170
-
171
- callback = callback === undefined ? null : callback;
172
- if (callback !== null) {
173
- warningWithoutStack(
174
- typeof callback === 'function',
175
- 'render(...): Expected the last optional `callback` argument to be a ' +
176
- 'function. Instead received: %s.',
177
- callback,
178
- );
179
- update.callback = callback;
180
- }
181
-
182
- enqueueUpdate(current, update);
183
- scheduleWork(current, expirationTime);
184
-
185
- return expirationTime;
186
-}
187
-
188
-export function updateContainerAtExpirationTime(
189
- element: ReactNodeList,
190
- container: OpaqueRoot,
191
- parentComponent: ?React$Component<any, any>,
192
- expirationTime: ExpirationTime,
193
- suspenseConfig: null | SuspenseConfig,
194
- callback: ?Function,
195
-) {
196
- // TODO: If this is a nested container, this won't be the root.
197
- const current = container.current;
198
-
199
- if (__DEV__) {
200
- if (ReactFiberInstrumentation.debugTool) {
201
- if (current.alternate === null) {
202
- ReactFiberInstrumentation.debugTool.onMountContainer(container);
203
- } else if (element === null) {
204
- ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
205
- } else {
206
- ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
207
- }
208
- }
209
- }
210
-
211
- const context = getContextForSubtree(parentComponent);
212
- if (container.context === null) {
213
- container.context = context;
214
- } else {
215
- container.pendingContext = context;
216
- }
217
-
218
- return scheduleRootUpdate(
219
- current,
220
- element,
221
- expirationTime,
222
- suspenseConfig,
223
- callback,
224
- );
225
-}
226
-
139
function findHostInstance(component: Object): PublicInstance | null {
140
const fiber = getInstance(component);
141
if (fiber === undefined) {
@@ -333,19 +245,68 @@ export function updateContainer(
245
current,
246
suspenseConfig,
247
);
336
- return updateContainerAtExpirationTime(
337
- element,
338
- container,
339
- parentComponent,
340
- expirationTime,
341
- suspenseConfig,
342
- callback,
343
- );
248
+
249
+ if (__DEV__) {
250
+ if (ReactFiberInstrumentation.debugTool) {
251
+ if (current.alternate === null) {
252
+ ReactFiberInstrumentation.debugTool.onMountContainer(container);
253
+ } else if (element === null) {
254
+ ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
255
+ } else {
256
+ ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
257
+ }
258
+ }
259
+ }
260
+
261
+ const context = getContextForSubtree(parentComponent);
262
+ if (container.context === null) {
263
+ container.context = context;
264
+ } else {
265
+ container.pendingContext = context;
266
+ }
267
+
268
+ if (__DEV__) {
269
+ if (
270
+ ReactCurrentFiberPhase === 'render' &&
271
+ ReactCurrentFiberCurrent !== null &&
272
+ !didWarnAboutNestedUpdates
273
+ ) {
274
+ didWarnAboutNestedUpdates = true;
275
+ warningWithoutStack(
276
+ false,
277
+ 'Render methods should be a pure function of props and state; ' +
278
+ 'triggering nested component updates from render is not allowed. ' +
279
+ 'If necessary, trigger nested updates in componentDidUpdate.\n\n' +
280
+ 'Check the render method of %s.',
281
+ getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown',
282
+ );
283
+ }
284
+ }
285
+
286
+ const update = createUpdate(expirationTime, suspenseConfig);
287
+ // Caution: React DevTools currently depends on this property
288
+ // being called "element".
289
+ update.payload = {element};
290
+
291
+ callback = callback === undefined ? null : callback;
292
+ if (callback !== null) {
293
+ warningWithoutStack(
294
+ typeof callback === 'function',
295
+ 'render(...): Expected the last optional `callback` argument to be a ' +
296
+ 'function. Instead received: %s.',
297
+ callback,
298
+ );
299
+ update.callback = callback;
300
+ }
301
+
302
+ enqueueUpdate(current, update);
303
+ scheduleWork(current, expirationTime);
304
+
305
+ return expirationTime;
306
}
307
308
export {
309
flushRoot,
348
- computeUniqueAsyncExpiration,
310
batchedEventUpdates,
311
batchedUpdates,
312
unbatchedUpdates,
packages/react-reconciler/src/ReactFiberRoot.js
-13
@@ -26,14 +26,6 @@ import {
26
import {unstable_getThreadID} from 'scheduler/tracing';
27
import {NoPriority} from './SchedulerWithReactIntegration';
28
29
-// TODO: This should be lifted into the renderer.
30
-export type Batch = {
31
- _defer: boolean,
32
- _expirationTime: ExpirationTime,
33
- _onComplete: () => mixed,
34
- _next: Batch | null,
35
-};
36
-
29
export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
30
31
type BaseFiberRootProperties = {|
@@ -63,10 +55,6 @@ type BaseFiberRootProperties = {|
55
pendingContext: Object | null,
56
// Determines if we should attempt to hydrate on the initial mount
57
+hydrate: boolean,
66
- // List of top-level batches. This list indicates whether a commit should be
67
- // deferred. Also contains completion callbacks.
68
- // TODO: Lift this into the renderer
69
- firstBatch: Batch | null,
58
// Node returned by Scheduler.scheduleCallback
59
callbackNode: *,
60
// Expiration of the callback associated with this root
@@ -125,7 +113,6 @@ function FiberRootNode(containerInfo, tag, hydrate) {
113
this.context = null;
114
this.pendingContext = null;
115
this.hydrate = hydrate;
128
- this.firstBatch = null;
116
this.callbackNode = null;
117
this.callbackPriority = NoPriority;
118
this.firstPendingTime = NoWork;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+8
-44
@@ -200,14 +200,13 @@ const LegacyUnbatchedContext = /* */ 0b001000;
200
const RenderContext = /* */ 0b010000;
201
const CommitContext = /* */ 0b100000;
202
203
-type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
203
+type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
204
const RootIncomplete = 0;
205
const RootFatalErrored = 1;
206
const RootErrored = 2;
207
const RootSuspended = 3;
208
const RootSuspendedWithDelay = 4;
209
const RootCompleted = 5;
210
-const RootLocked = 6;
210
211
export type Thenable = {
212
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
@@ -719,7 +718,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
718
const finishedWork: Fiber = ((root.finishedWork =
719
root.current.alternate): any);
720
root.finishedExpirationTime = expirationTime;
722
- resolveLocksOnRoot(root, expirationTime);
721
finishConcurrentRender(
722
root,
723
finishedWork,
@@ -976,13 +974,6 @@ function finishConcurrentRender(
974
commitRoot(root);
975
break;
976
}
979
- case RootLocked: {
980
- // This root has a lock that prevents it from committing. Exit. If
981
- // we begin work on the root again, without any intervening updates,
982
- // it will finish without doing additional work.
983
- markRootSuspendedAtTime(root, expirationTime);
984
- break;
985
- }
977
default: {
978
invariant(false, 'Unknown root exit status.');
979
}
@@ -1060,12 +1051,10 @@ function performSyncWorkOnRoot(root) {
1051
);
1052
} else {
1053
// We now have a consistent tree. Because this is a sync render, we
1063
- // will commit it even if something suspended. The only exception is
1064
- // if the root is locked (using the unstable_createBatch API).
1054
+ // will commit it even if something suspended.
1055
stopFinishedWorkLoopTimer();
1056
root.finishedWork = (root.current.alternate: any);
1057
root.finishedExpirationTime = expirationTime;
1068
- resolveLocksOnRoot(root, expirationTime);
1058
finishSyncRender(root, workInProgressRootExitStatus, expirationTime);
1059
}
1060
@@ -1079,25 +1068,15 @@ function performSyncWorkOnRoot(root) {
1068
}
1069
1070
function finishSyncRender(root, exitStatus, expirationTime) {
1082
- if (exitStatus === RootLocked) {
1083
- // This root has a lock that prevents it from committing. Exit. If we
1084
- // begin work on the root again, without any intervening updates, it
1085
- // will finish without doing additional work.
1086
- markRootSuspendedAtTime(root, expirationTime);
1087
- } else {
1088
- // Set this to null to indicate there's no in-progress render.
1089
- workInProgressRoot = null;
1071
+ // Set this to null to indicate there's no in-progress render.
1072
+ workInProgressRoot = null;
1073
1091
- if (__DEV__) {
1092
- if (
1093
- exitStatus === RootSuspended ||
1094
- exitStatus === RootSuspendedWithDelay
1095
- ) {
1096
- flushSuspensePriorityWarningInDEV();
1097
- }
1074
+ if (__DEV__) {
1075
+ if (exitStatus === RootSuspended || exitStatus === RootSuspendedWithDelay) {
1076
+ flushSuspensePriorityWarningInDEV();
1077
}
1099
- commitRoot(root);
1078
}
1079
+ commitRoot(root);
1080
}
1081
1082
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -1140,21 +1119,6 @@ export function flushDiscreteUpdates() {
1119
flushPassiveEffects();
1120
}
1121
1143
-function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1144
- const firstBatch = root.firstBatch;
1145
- if (
1146
- firstBatch !== null &&
1147
- firstBatch._defer &&
1148
- firstBatch._expirationTime >= expirationTime
1149
- ) {
1150
- scheduleCallback(NormalPriority, () => {
1151
- firstBatch._onComplete();
1152
- return null;
1153
- });
1154
- workInProgressRootExitStatus = RootLocked;
1155
- }
1156
-}
1157
-
1122
export function deferredUpdates<A>(fn: () => A): A {
1123
// TODO: Remove in favor of Scheduler.next
1124
return runWithPriority(NormalPriority, fn);
packages/react/src/__tests__/ReactProfiler-test.internal.js
-74
@@ -97,15 +97,6 @@ function loadModules({
97
};
98
}
99
100
-const mockDevToolsForTest = () => {
101
- jest.mock('react-reconciler/src/ReactFiberDevToolsHook', () => ({
102
- injectInternals: () => {},
103
- onCommitRoot: () => {},
104
- onCommitUnmount: () => {},
105
- isDevToolsPresent: true,
106
- }));
107
-};
108
-
100
describe('Profiler', () => {
101
describe('works in profiling and non-profiling bundles', () => {
102
[true, false].forEach(enableSchedulerTracing => {
@@ -1205,71 +1196,6 @@ describe('Profiler', () => {
1196
});
1197
});
1198
1208
- it('should handle interleaved async yields and batched commits', () => {
1209
- jest.resetModules();
1210
- mockDevToolsForTest();
1211
- loadModules({useNoopRenderer: true});
1212
-
1213
- const Child = ({duration, id}) => {
1214
- Scheduler.unstable_advanceTime(duration);
1215
- Scheduler.unstable_yieldValue(`Child:render:${id}`);
1216
- return null;
1217
- };
1218
-
1219
- class Parent extends React.Component {
1220
- componentDidMount() {
1221
- Scheduler.unstable_yieldValue(
1222
- `Parent:componentDidMount:${this.props.id}`,
1223
- );
1224
- }
1225
- render() {
1226
- const {duration, id} = this.props;
1227
- return (
1228
- <>
1229
- <Child duration={duration} id={id} />
1230
- <Child duration={duration} id={id} />
1231
- </>
1232
- );
1233
- }
1234
- }
1235
-
1236
- Scheduler.unstable_advanceTime(50);
1237
-
1238
- ReactNoop.renderToRootWithID(<Parent duration={3} id="one" />, 'one');
1239
-
1240
- // Process up to the <Parent> component, but yield before committing.
1241
- // This ensures that the profiler timer still has paused fibers.
1242
- const commitFirstRender = ReactNoop.flushWithoutCommitting(
1243
- ['Child:render:one', 'Child:render:one'],
1244
- 'one',
1245
- );
1246
-
1247
- expect(ReactNoop.getRoot('one').current.actualDuration).toBe(0);
1248
-
1249
- Scheduler.unstable_advanceTime(100);
1250
-
1251
- // Process some async work, but yield before committing it.
1252
- ReactNoop.renderToRootWithID(<Parent duration={7} id="two" />, 'two');
1253
- expect(Scheduler).toFlushAndYieldThrough(['Child:render:two']);
1254
-
1255
- Scheduler.unstable_advanceTime(150);
1256
-
1257
- // Commit the previously paused, batched work.
1258
- commitFirstRender(['Parent:componentDidMount:one']);
1259
-
1260
- expect(ReactNoop.getRoot('one').current.actualDuration).toBe(6);
1261
- expect(ReactNoop.getRoot('two').current.actualDuration).toBe(0);
1262
-
1263
- Scheduler.unstable_advanceTime(200);
1264
-
1265
- expect(Scheduler).toFlushAndYield([
1266
- 'Child:render:two',
1267
- 'Parent:componentDidMount:two',
1268
- ]);
1269
-
1270
- expect(ReactNoop.getRoot('two').current.actualDuration).toBe(14);
1271
- });
1272
-
1199
describe('interaction tracing', () => {
1200
let onInteractionScheduledWorkCompleted;
1201
let onInteractionTraced;
packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
+60
-78
@@ -14,7 +14,6 @@ let ReactFeatureFlags;
14
let ReactDOM;
15
let SchedulerTracing;
16
let Scheduler;
17
-let ReactCache;
17
18
function loadModules() {
19
ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -27,12 +26,9 @@ function loadModules() {
26
SchedulerTracing = require('scheduler/tracing');
27
ReactDOM = require('react-dom');
28
Scheduler = require('scheduler');
30
- ReactCache = require('react-cache');
29
}
30
31
describe('ProfilerDOM', () => {
34
- let TextResource;
35
- let resourcePromise;
32
let onInteractionScheduledWorkCompleted;
33
let onInteractionTraced;
34
@@ -51,97 +47,83 @@ describe('ProfilerDOM', () => {
47
onWorkStarted: () => {},
48
onWorkStopped: () => {},
49
});
54
-
55
- resourcePromise = null;
56
-
57
- TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
58
- resourcePromise = new Promise(
59
- SchedulerTracing.unstable_wrap((resolve, reject) => {
60
- setTimeout(
61
- SchedulerTracing.unstable_wrap(() => {
62
- resolve(text);
63
- }),
64
- ms,
65
- );
66
- }),
67
- );
68
- return resourcePromise;
69
- }, ([text, ms]) => text);
50
});
51
72
- const AsyncText = ({ms, text}) => {
73
- TextResource.read([text, ms]);
74
- return text;
75
- };
76
-
77
- const Text = ({text}) => text;
78
-
79
- it('should correctly trace interactions for async roots', async done => {
80
- let batch, element, interaction;
81
-
52
+ function Text(props) {
53
+ Scheduler.unstable_yieldValue(props.text);
54
+ return props.text;
55
+ }
56
+
57
+ it('should correctly trace interactions for async roots', async () => {
58
+ let resolve;
59
+ let thenable = {
60
+ then(res) {
61
+ resolve = () => {
62
+ thenable = null;
63
+ res();
64
+ };
65
+ },
66
+ };
67
+
68
+ function Async() {
69
+ if (thenable !== null) {
70
+ Scheduler.unstable_yieldValue('Suspend! [Async]');
71
+ throw thenable;
72
+ }
73
+ Scheduler.unstable_yieldValue('Async');
74
+ return 'Async';
75
+ }
76
+
77
+ const element = document.createElement('div');
78
+ const root = ReactDOM.unstable_createRoot(element);
79
+
80
+ let interaction;
81
+ let wrappedResolve;
82
SchedulerTracing.unstable_trace('initial_event', performance.now(), () => {
83
const interactions = SchedulerTracing.unstable_getCurrent();
84
expect(interactions.size).toBe(1);
85
interaction = Array.from(interactions)[0];
86
87
- element = document.createElement('div');
88
- const root = ReactDOM.unstable_createRoot(element);
89
- batch = root.createBatch();
90
- batch.render(
87
+ root.render(
88
<React.Suspense fallback={<Text text="Loading..." />}>
92
- <AsyncText text="Text" ms={2000} />
89
+ <Async />
90
</React.Suspense>,
91
);
95
- batch.then(
96
- SchedulerTracing.unstable_wrap(() => {
97
- batch.commit();
98
-
99
- expect(element.textContent).toBe('Loading...');
100
- expect(onInteractionTraced).toHaveBeenCalledTimes(1);
101
- expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
102
-
103
- jest.runAllTimers();
104
-
105
- resourcePromise.then(
106
- SchedulerTracing.unstable_wrap(() => {
107
- jest.runAllTimers();
108
- Scheduler.unstable_flushAll();
109
-
110
- expect(element.textContent).toBe('Text');
111
- expect(onInteractionTraced).toHaveBeenCalledTimes(1);
112
- expect(
113
- onInteractionScheduledWorkCompleted,
114
- ).not.toHaveBeenCalled();
115
-
116
- // Evaluate in an unwrapped callback,
117
- // Because trace/wrap won't decrement the count within the wrapped callback.
118
- Promise.resolve().then(() => {
119
- expect(onInteractionTraced).toHaveBeenCalledTimes(1);
120
- expect(
121
- onInteractionScheduledWorkCompleted,
122
- ).toHaveBeenCalledTimes(1);
123
- expect(
124
- onInteractionScheduledWorkCompleted,
125
- ).toHaveBeenLastNotifiedOfInteraction(interaction);
126
-
127
- expect(interaction.__count).toBe(0);
128
-
129
- done();
130
- });
131
- }),
132
- );
133
- }),
134
- );
92
136
- Scheduler.unstable_flushAll();
93
+ wrappedResolve = SchedulerTracing.unstable_wrap(() => resolve());
94
});
95
96
+ // Render, suspend, and commit fallback
97
+ expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
98
+ expect(element.textContent).toEqual('Loading...');
99
+
100
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
101
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
102
interaction,
103
);
104
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
144
- Scheduler.unstable_flushAll();
145
- jest.advanceTimersByTime(500);
105
+
106
+ // Ping React to try rendering again
107
+ wrappedResolve();
108
+
109
+ // Complete the tree without committing it
110
+ expect(Scheduler).toFlushAndYieldThrough(['Async']);
111
+ // Still showing the fallback
112
+ expect(element.textContent).toEqual('Loading...');
113
+
114
+ expect(onInteractionTraced).toHaveBeenCalledTimes(1);
115
+ expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
116
+ interaction,
117
+ );
118
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
119
+
120
+ expect(Scheduler).toFlushAndYield([]);
121
+ expect(element.textContent).toEqual('Async');
122
+
123
+ expect(onInteractionTraced).toHaveBeenCalledTimes(1);
124
+ expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
125
+ interaction,
126
+ );
127
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
128
});
129
});