Skip to content

Commit

Permalink
root.createBatch (#11473)
Browse files Browse the repository at this point in the history
API for batching top-level updates and deferring the commit.

- `root.createBatch` creates a batch with an async expiration time
  associated with it.
- `batch.render` updates the children that the batch renders.
- `batch.then` resolves when the root has completed.
- `batch.commit` synchronously flushes any remaining work and commits.

No two batches can have the same expiration time. The only way to
commit a batch is by calling its `commit` method. E.g. flushing one
batch will not cause a different batch to also flush.
  • Loading branch information
acdlite authored Nov 29, 2017
1 parent 9895a0f commit 1b55ad2
Show file tree
Hide file tree
Showing 6 changed files with 870 additions and 169 deletions.
231 changes: 230 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMRoot-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,68 @@

'use strict';

require('shared/ReactFeatureFlags').enableCreateRoot = true;
var React = require('react');
var ReactDOM = require('react-dom');
var ReactDOMServer = require('react-dom/server');
var AsyncComponent = React.unstable_AsyncComponent;

describe('ReactDOMRoot', () => {
let container;

let scheduledCallback;
let flush;
let now;
let expire;

beforeEach(() => {
container = document.createElement('div');

// Override requestIdleCallback
scheduledCallback = null;
flush = function(units = Infinity) {
if (scheduledCallback !== null) {
var didStop = false;
while (scheduledCallback !== null && !didStop) {
const cb = scheduledCallback;
scheduledCallback = null;
cb({
timeRemaining() {
if (units > 0) {
return 999;
}
didStop = true;
return 0;
},
});
units--;
}
}
};
global.performance = {
now() {
return now;
},
};
global.requestIdleCallback = function(cb) {
scheduledCallback = cb;
};

now = 0;
expire = function(ms) {
now += ms;
};
global.performance = {
now() {
return now;
},
};

jest.resetModules();
require('shared/ReactFeatureFlags').enableCreateRoot = true;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
AsyncComponent = React.unstable_AsyncComponent;
});

it('renders children', () => {
Expand All @@ -35,6 +87,35 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('');
});

it('`root.render` returns a thenable work object', () => {
const root = ReactDOM.createRoot(container);
const work = root.render(<AsyncComponent>Hi</AsyncComponent>);
let ops = [];
work.then(() => {
ops.push('inside callback: ' + container.textContent);
});
ops.push('before committing: ' + container.textContent);
flush();
ops.push('after committing: ' + container.textContent);
expect(ops).toEqual([
'before committing: ',
// `then` callback should fire during commit phase
'inside callback: Hi',
'after committing: Hi',
]);
});

it('resolves `work.then` callback synchronously if the work already committed', () => {
const root = ReactDOM.createRoot(container);
const work = root.render(<AsyncComponent>Hi</AsyncComponent>);
flush();
let ops = [];
work.then(() => {
ops.push('inside callback');
});
expect(ops).toEqual(['inside callback']);
});

it('supports hydration', async () => {
const markup = await new Promise(resolve =>
resolve(
Expand Down Expand Up @@ -95,4 +176,152 @@ describe('ReactDOMRoot', () => {
);
expect(container.textContent).toEqual('abdc');
});

it('can defer a commit by batching it', () => {
const root = ReactDOM.createRoot(container);
const batch = root.createBatch();
batch.render(<div>Hi</div>);
// Hasn't committed yet
expect(container.textContent).toEqual('');
// Commit
batch.commit();
expect(container.textContent).toEqual('Hi');
});

it('does not restart a completed batch when committing if there were no intervening updates', () => {
let ops = [];
function Foo(props) {
ops.push('Foo');
return props.children;
}
const root = ReactDOM.createRoot(container);
const batch = root.createBatch();
batch.render(<Foo>Hi</Foo>);
// Flush all async work.
flush();
// Root should complete without committing.
expect(ops).toEqual(['Foo']);
expect(container.textContent).toEqual('');

ops = [];

// Commit. Shouldn't re-render Foo.
batch.commit();
expect(ops).toEqual([]);
expect(container.textContent).toEqual('Hi');
});

it('can wait for a batch to finish', () => {
const Async = React.unstable_AsyncComponent;
const root = ReactDOM.createRoot(container);
const batch = root.createBatch();
batch.render(<Async>Foo</Async>);

flush();

// Hasn't updated yet
expect(container.textContent).toEqual('');

let ops = [];
batch.then(() => {
// Still hasn't updated
ops.push(container.textContent);
// Should synchronously commit
batch.commit();
ops.push(container.textContent);
});

expect(ops).toEqual(['', 'Foo']);
});

it('`batch.render` returns a thenable work object', () => {
const root = ReactDOM.createRoot(container);
const batch = root.createBatch();
const work = batch.render('Hi');
let ops = [];
work.then(() => {
ops.push('inside callback: ' + container.textContent);
});
ops.push('before committing: ' + container.textContent);
batch.commit();
ops.push('after committing: ' + container.textContent);
expect(ops).toEqual([
'before committing: ',
// `then` callback should fire during commit phase
'inside callback: Hi',
'after committing: Hi',
]);
});

it('can commit an empty batch', () => {
const root = ReactDOM.createRoot(container);
root.render(<AsyncComponent>1</AsyncComponent>);

expire(2000);
// This batch has a later expiration time than the earlier update.
const batch = root.createBatch();

// This should not flush the earlier update.
batch.commit();
expect(container.textContent).toEqual('');

flush();
expect(container.textContent).toEqual('1');
});

it('two batches created simultaneously are committed separately', () => {
// (In other words, they have distinct expiration times)
const root = ReactDOM.createRoot(container);
const batch1 = root.createBatch();
batch1.render(1);
const batch2 = root.createBatch();
batch2.render(2);

expect(container.textContent).toEqual('');

batch1.commit();
expect(container.textContent).toEqual('1');

batch2.commit();
expect(container.textContent).toEqual('2');
});

it('commits an earlier batch without committing a later batch', () => {
const root = ReactDOM.createRoot(container);
const batch1 = root.createBatch();
batch1.render(1);

// This batch has a later expiration time
expire(2000);
const batch2 = root.createBatch();
batch2.render(2);

expect(container.textContent).toEqual('');

batch1.commit();
expect(container.textContent).toEqual('1');

batch2.commit();
expect(container.textContent).toEqual('2');
});

it('commits a later batch without commiting an earlier batch', () => {
const root = ReactDOM.createRoot(container);
const batch1 = root.createBatch();
batch1.render(1);

// This batch has a later expiration time
expire(2000);
const batch2 = root.createBatch();
batch2.render(2);

expect(container.textContent).toEqual('');

batch2.commit();
expect(container.textContent).toEqual('2');

batch1.commit();
flush();
expect(container.textContent).toEqual('1');
});
});
Loading

0 comments on commit 1b55ad2

Please sign in to comment.