Skip to content

Commit 5a4bbc5

Browse files
committed
Use unbatchedUpdates to opt-out of batching
Reverses the effect of batchedUpdates by resetting the current batching context. Does not affect nested updates, which are always deferred regardless of whether they are inside a batch.
1 parent b79531e commit 5a4bbc5

File tree

6 files changed

+80
-7
lines changed

6 files changed

+80
-7
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,8 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js
12071207
* can opt-in to deferred/animation scheduling inside componentDidMount/Update
12081208
* performs Task work even after time runs out
12091209
* does not perform animation work after time runs out
1210+
* can opt-out of batching using unbatchedUpdates
1211+
* nested updates are always deferred, even inside unbatchedUpdates
12101212

12111213
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
12121214
* can update child nodes of a host instance

src/renderers/dom/fiber/ReactDOMFiber.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ function renderSubtreeIntoContainer(parentComponent : ?ReactComponent<any, any,
326326
}
327327
const newRoot = DOMRenderer.createContainer(container);
328328
root = container._reactRootContainer = newRoot;
329-
// Initial mount is always sync, even if we're in a batch.
330-
DOMRenderer.syncUpdates(() => {
329+
// Initial mount should not be batched.
330+
DOMRenderer.unbatchedUpdates(() => {
331331
DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);
332332
});
333333
} else {
@@ -354,8 +354,8 @@ var ReactDOM = {
354354
unmountComponentAtNode(container : DOMContainerElement) {
355355
warnAboutUnstableUse();
356356
if (container._reactRootContainer) {
357-
// Unmount is always sync, even if we're in a batch.
358-
return DOMRenderer.syncUpdates(() => {
357+
// Unmount should not be batched.
358+
return DOMRenderer.unbatchedUpdates(() => {
359359
return renderSubtreeIntoContainer(null, null, container, () => {
360360
container._reactRootContainer = null;
361361
});

src/renderers/noop/ReactNoop.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ var ReactNoop = {
264264

265265
batchedUpdates: NoopRenderer.batchedUpdates,
266266

267+
unbatchedUpdates: NoopRenderer.unbatchedUpdates,
268+
267269
syncUpdates: NoopRenderer.syncUpdates,
268270

269271
// Logs the current state of the tree.

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export type Reconciler<C, I, TI> = {
8282
/* eslint-disable no-undef */
8383
// FIXME: ESLint complains about type parameter
8484
batchedUpdates<A>(fn : () => A) : A,
85+
unbatchedUpdates<A>(fn : () => A) : A,
8586
syncUpdates<A>(fn : () => A) : A,
8687
deferredUpdates<A>(fn : () => A) : A,
8788
/* eslint-enable no-undef */
@@ -107,6 +108,7 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
107108
getPriorityContext,
108109
performWithPriority,
109110
batchedUpdates,
111+
unbatchedUpdates,
110112
syncUpdates,
111113
deferredUpdates,
112114
} = ReactFiberScheduler(config);
@@ -161,6 +163,8 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
161163

162164
batchedUpdates,
163165

166+
unbatchedUpdates,
167+
164168
syncUpdates,
165169

166170
deferredUpdates,

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,16 +1094,23 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
10941094
}
10951095
}
10961096

1097+
function unbatchedUpdates<A>(fn : () => A) : A {
1098+
const previousIsBatchingUpdates = isBatchingUpdates;
1099+
isBatchingUpdates = false;
1100+
try {
1101+
return fn();
1102+
} finally {
1103+
isBatchingUpdates = previousIsBatchingUpdates;
1104+
}
1105+
}
1106+
10971107
function syncUpdates<A>(fn : () => A) : A {
10981108
const previousPriorityContext = priorityContext;
1099-
const previousIsBatchingUpdates = isBatchingUpdates;
11001109
priorityContext = SynchronousPriority;
1101-
isBatchingUpdates = false;
11021110
try {
11031111
return fn();
11041112
} finally {
11051113
priorityContext = previousPriorityContext;
1106-
isBatchingUpdates = previousIsBatchingUpdates;
11071114
}
11081115
}
11091116

@@ -1122,6 +1129,7 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
11221129
getPriorityContext: getPriorityContext,
11231130
performWithPriority: performWithPriority,
11241131
batchedUpdates: batchedUpdates,
1132+
unbatchedUpdates: unbatchedUpdates,
11251133
syncUpdates: syncUpdates,
11261134
deferredUpdates: deferredUpdates,
11271135
};

src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,61 @@ describe('ReactIncrementalScheduling', () => {
340340
// animation priority.
341341
expect(ReactNoop.getChildren()).toEqual([span(1)]);
342342
});
343+
344+
it('can opt-out of batching using unbatchedUpdates', () => {
345+
// syncUpdates gives synchronous priority to updates
346+
ReactNoop.syncUpdates(() => {
347+
// batchedUpdates downgrades sync updates to task priority
348+
ReactNoop.batchedUpdates(() => {
349+
ReactNoop.render(<span prop={0} />);
350+
expect(ReactNoop.getChildren()).toEqual([]);
351+
// Should not have flushed yet because we're still batching
352+
353+
// unbatchedUpdates reverses the effect of batchedUpdates, so sync
354+
// updates are not batched
355+
ReactNoop.unbatchedUpdates(() => {
356+
ReactNoop.render(<span prop={1} />);
357+
expect(ReactNoop.getChildren()).toEqual([span(1)]);
358+
ReactNoop.render(<span prop={2} />);
359+
expect(ReactNoop.getChildren()).toEqual([span(2)]);
360+
});
361+
362+
ReactNoop.render(<span prop={3} />);
363+
expect(ReactNoop.getChildren()).toEqual([span(2)]);
364+
});
365+
// Remaining update is now flushed
366+
expect(ReactNoop.getChildren()).toEqual([span(3)]);
367+
});
368+
});
369+
370+
it('nested updates are always deferred, even inside unbatchedUpdates', done => {
371+
let instance;
372+
class Foo extends React.Component {
373+
state = { step: 0 };
374+
componentDidUpdate() {
375+
if (this.state.step === 1) {
376+
ReactNoop.unbatchedUpdates(() => {
377+
// This is a nested state update, so it should not be
378+
// flushed synchronously, even though we wrapped it
379+
// in unbatchedUpdates.
380+
this.setState({ step: 2 });
381+
});
382+
expect(ReactNoop.getChildren()).toEqual([span(1)]);
383+
done();
384+
}
385+
}
386+
render() {
387+
instance = this;
388+
return <span prop={this.state.step} />;
389+
}
390+
}
391+
ReactNoop.render(<Foo />);
392+
ReactNoop.flush();
393+
expect(ReactNoop.getChildren()).toEqual([span(0)]);
394+
395+
ReactNoop.syncUpdates(() => {
396+
instance.setState({ step: 1 });
397+
expect(ReactNoop.getChildren()).toEqual([span(2)]);
398+
});
399+
});
343400
});

0 commit comments

Comments
 (0)