@samitouri / QOS-React-2 / commits / 71d012ecd0

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 });