@samitouri / QOS-React-2 / commits / 3e08e60a34

ReactDOM.flushControlled (#12118)

* ReactDOM.flushControlled New API for wrapping event handlers that need to fire before React yields to the browser. Previously we thought that flushSync was sufficient for this use case, but it turns out that flushSync is only safe if you're guaranteed to be at the top of the stack; that is, if you know for sure that your event handler is not nested inside another React event handler or lifecycle. This isn't true for cases like el.focus, el.click, or dispatchEvent, where an event handler can be invoked synchronously from inside an existing stack. flushControlled has similar semantics to batchedUpdates, where if you nest multiple batches, the work is not flushed until the end of the outermost batch. The work is not guaranteed to synchronously flush, as with flushSync, but it is guaranteed to flush before React yields to the browser. flushSync is still the preferred API in most cases, such as inside a requestAnimationFrame callback. * Test that flushControlled does not flush inside batchedUpdates * Make flushControlled a void function In the future, we may want to return a thenable work object. For now, we'll return nothing. * flushControlled -> unstable_flushControlled

Andrew Clark committed Jan 29, 2018 at 22:36 UTC 3e08e60a3454395a2510d718c0b7d796548b3bd9
4 files changed +116
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
+96
@@ -67,6 +67,7 @@ describe('ReactDOMFiberAsync', () => {
67 jest.resetModules();
68 ReactFeatureFlags = require('shared/ReactFeatureFlags');
69 container = document.createElement('div');
70 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
71 ReactFeatureFlags.enableCreateRoot = true;
72 ReactDOM = require('react-dom');
73 });
@@ -305,5 +306,100 @@ describe('ReactDOMFiberAsync', () => {
306 expect(container.textContent).toEqual('ABCD');
307 expect(ops).toEqual(['BC', 'ABCD']);
308 });
309 +
310 + it('flushControlled flushes updates before yielding to browser', () => {
311 + let inst;
312 + class Counter extends React.Component {
313 + state = {counter: 0};
314 + increment = () =>
315 + this.setState(state => ({counter: state.counter + 1}));
316 + render() {
317 + inst = this;
318 + return this.state.counter;
319 + }
320 + }
321 + ReactDOM.render(
322 + <AsyncMode>
323 + <Counter />
324 + </AsyncMode>,
325 + container,
326 + );
327 + expect(container.textContent).toEqual('0');
328 +
329 + // Test that a normal update is async
330 + inst.increment();
331 + expect(container.textContent).toEqual('0');
332 + jest.runAllTimers();
333 + expect(container.textContent).toEqual('1');
334 +
335 + let ops = [];
336 + ReactDOM.unstable_flushControlled(() => {
337 + inst.increment();
338 + ReactDOM.unstable_flushControlled(() => {
339 + inst.increment();
340 + ops.push('end of inner flush: ' + container.textContent);
341 + });
342 + ops.push('end of outer flush: ' + container.textContent);
343 + });
344 + ops.push('after outer flush: ' + container.textContent);
345 + expect(ops).toEqual([
346 + 'end of inner flush: 1',
347 + 'end of outer flush: 1',
348 + 'after outer flush: 3',
349 + ]);
350 + });
351 +
352 + it('flushControlled does not flush until end of outermost batchedUpdates', () => {
353 + let inst;
354 + class Counter extends React.Component {
355 + state = {counter: 0};
356 + increment = () =>
357 + this.setState(state => ({counter: state.counter + 1}));
358 + render() {
359 + inst = this;
360 + return this.state.counter;
361 + }
362 + }
363 + ReactDOM.render(<Counter />, container);
364 +
365 + let ops = [];
366 + ReactDOM.unstable_batchedUpdates(() => {
367 + inst.increment();
368 + ReactDOM.unstable_flushControlled(() => {
369 + inst.increment();
370 + ops.push('end of flushControlled fn: ' + container.textContent);
371 + });
372 + ops.push('end of batchedUpdates fn: ' + container.textContent);
373 + });
374 + ops.push('after batchedUpdates: ' + container.textContent);
375 + expect(ops).toEqual([
376 + 'end of flushControlled fn: 0',
377 + 'end of batchedUpdates fn: 0',
378 + 'after batchedUpdates: 2',
379 + ]);
380 + });
381 +
382 + it('flushControlled returns nothing', () => {
383 + // In the future, we may want to return a thenable "work" object.
384 + let inst;
385 + class Counter extends React.Component {
386 + state = {counter: 0};
387 + increment = () =>
388 + this.setState(state => ({counter: state.counter + 1}));
389 + render() {
390 + inst = this;
391 + return this.state.counter;
392 + }
393 + }
394 + ReactDOM.render(<Counter />, container);
395 + expect(container.textContent).toEqual('0');
396 +
397 + const returnValue = ReactDOM.unstable_flushControlled(() => {
398 + inst.increment();
399 + return 'something';
400 + });
401 + expect(container.textContent).toEqual('1');
402 + expect(returnValue).toBe(undefined);
403 + });
404 });
405 });
packages/react-dom/src/client/ReactDOM.js
+2
@@ -1288,6 +1288,8 @@ const ReactDOM: Object = {
1288
1289 flushSync: DOMRenderer.flushSync,
1290
1291 + unstable_flushControlled: DOMRenderer.flushControlled,
1292 +
1293 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
1294 // For TapEventPlugin which is popular in open source
1295 EventPluginHub,
packages/react-reconciler/src/ReactFiberReconciler.js
+4
@@ -256,6 +256,7 @@ export type Reconciler<C, I, TI> = {
256 batchedUpdates<A>(fn: () => A): A,
257 unbatchedUpdates<A>(fn: () => A): A,
258 flushSync<A>(fn: () => A): A,
259 + flushControlled(fn: () => mixed): void,
260 deferredUpdates<A>(fn: () => A): A,
261 injectIntoDevTools(devToolsConfig: DevToolsConfig<I, TI>): boolean,
262 computeUniqueAsyncExpiration(): ExpirationTime,
@@ -300,6 +301,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
301 batchedUpdates,
302 unbatchedUpdates,
303 flushSync,
304 + flushControlled,
305 deferredUpdates,
306 } = ReactFiberScheduler(config);
307
@@ -433,6 +435,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
435
436 flushSync,
437
438 + flushControlled,
439 +
440 getPublicRootInstance(
441 container: OpaqueRoot,
442 ): React$Component<any, any> | PI | null {
packages/react-reconciler/src/ReactFiberScheduler.js
+14
@@ -1764,6 +1764,19 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1764 }
1765 }
1766
1767 + function flushControlled(fn: () => mixed): void {
1768 + const previousIsBatchingUpdates = isBatchingUpdates;
1769 + isBatchingUpdates = true;
1770 + try {
1771 + syncUpdates(fn);
1772 + } finally {
1773 + isBatchingUpdates = previousIsBatchingUpdates;
1774 + if (!isBatchingUpdates && !isRendering) {
1775 + performWork(Sync, null);
1776 + }
1777 + }
1778 + }
1779 +
1780 return {
1781 computeExpirationForFiber,
1782 scheduleWork,
@@ -1772,6 +1785,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1785 batchedUpdates,
1786 unbatchedUpdates,
1787 flushSync,
1788 + flushControlled,
1789 deferredUpdates,
1790 computeUniqueAsyncExpiration,
1791 };