diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 5e820d1072..c60907f1d2 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1207,6 +1207,8 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js * can opt-in to deferred/animation scheduling inside componentDidMount/Update * performs Task work even after time runs out * does not perform animation work after time runs out +* can opt-out of batching using unbatchedUpdates +* nested updates are always deferred, even inside unbatchedUpdates src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js * can update child nodes of a host instance diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 26c3734909..ed27da5ee9 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -326,8 +326,8 @@ function renderSubtreeIntoContainer(parentComponent : ?ReactComponent { + // Initial mount should not be batched. + DOMRenderer.unbatchedUpdates(() => { DOMRenderer.updateContainer(children, newRoot, parentComponent, callback); }); } else { @@ -354,8 +354,8 @@ var ReactDOM = { unmountComponentAtNode(container : DOMContainerElement) { warnAboutUnstableUse(); if (container._reactRootContainer) { - // Unmount is always sync, even if we're in a batch. - return DOMRenderer.syncUpdates(() => { + // Unmount should not be batched. + return DOMRenderer.unbatchedUpdates(() => { return renderSubtreeIntoContainer(null, null, container, () => { container._reactRootContainer = null; }); diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 804aba156f..b1f2f92a8d 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -264,6 +264,8 @@ var ReactNoop = { batchedUpdates: NoopRenderer.batchedUpdates, + unbatchedUpdates: NoopRenderer.unbatchedUpdates, + syncUpdates: NoopRenderer.syncUpdates, // Logs the current state of the tree. diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 3424ef683f..68052a1db0 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -82,6 +82,7 @@ export type Reconciler = { /* eslint-disable no-undef */ // FIXME: ESLint complains about type parameter batchedUpdates(fn : () => A) : A, + unbatchedUpdates(fn : () => A) : A, syncUpdates(fn : () => A) : A, deferredUpdates(fn : () => A) : A, /* eslint-enable no-undef */ @@ -107,6 +108,7 @@ module.exports = function(config : HostConfig(config : HostConfig(config : HostConfig(fn : () => A) : A { - const previousPriorityContext = priorityContext; + function unbatchedUpdates(fn : () => A) : A { const previousIsBatchingUpdates = isBatchingUpdates; - priorityContext = SynchronousPriority; isBatchingUpdates = false; try { return fn(); } finally { - priorityContext = previousPriorityContext; isBatchingUpdates = previousIsBatchingUpdates; } } + function syncUpdates(fn : () => A) : A { + const previousPriorityContext = priorityContext; + priorityContext = SynchronousPriority; + try { + return fn(); + } finally { + priorityContext = previousPriorityContext; + } + } + function deferredUpdates(fn : () => A) : A { const previousPriorityContext = priorityContext; priorityContext = LowPriority; @@ -1122,6 +1129,7 @@ module.exports = function(config : HostConfig { // animation priority. expect(ReactNoop.getChildren()).toEqual([span(1)]); }); + + it('can opt-out of batching using unbatchedUpdates', () => { + // syncUpdates gives synchronous priority to updates + ReactNoop.syncUpdates(() => { + // batchedUpdates downgrades sync updates to task priority + ReactNoop.batchedUpdates(() => { + ReactNoop.render(); + expect(ReactNoop.getChildren()).toEqual([]); + // Should not have flushed yet because we're still batching + + // unbatchedUpdates reverses the effect of batchedUpdates, so sync + // updates are not batched + ReactNoop.unbatchedUpdates(() => { + ReactNoop.render(); + expect(ReactNoop.getChildren()).toEqual([span(1)]); + ReactNoop.render(); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + }); + + ReactNoop.render(); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + }); + // Remaining update is now flushed + expect(ReactNoop.getChildren()).toEqual([span(3)]); + }); + }); + + it('nested updates are always deferred, even inside unbatchedUpdates', () => { + let instance; + let ops = []; + class Foo extends React.Component { + state = { step: 0 }; + componentDidUpdate() { + ops.push('componentDidUpdate: ' + this.state.step); + if (this.state.step === 1) { + ReactNoop.unbatchedUpdates(() => { + // This is a nested state update, so it should not be + // flushed synchronously, even though we wrapped it + // in unbatchedUpdates. + this.setState({ step: 2 }); + }); + expect(ReactNoop.getChildren()).toEqual([span(1)]); + } + } + render() { + ops.push('render: ' + this.state.step); + instance = this; + return ; + } + } + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span(0)]); + + ReactNoop.syncUpdates(() => { + instance.setState({ step: 1 }); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + }); + + expect(ops).toEqual([ + 'render: 0', + 'render: 1', + 'componentDidUpdate: 1', + 'render: 2', + 'componentDidUpdate: 2', + ]); + }); });