mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
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
This commit is contained in:
@@ -43,35 +43,6 @@ describe('ReactDOMRoot', () => {
|
||||
expect(container.textContent).toEqual('');
|
||||
});
|
||||
|
||||
it('`root.render` returns a thenable work object', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const work = root.render('Hi');
|
||||
let ops = [];
|
||||
work.then(() => {
|
||||
ops.push('inside callback: ' + container.textContent);
|
||||
});
|
||||
ops.push('before committing: ' + container.textContent);
|
||||
Scheduler.unstable_flushAll();
|
||||
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.unstable_createRoot(container);
|
||||
const work = root.render('Hi');
|
||||
Scheduler.unstable_flushAll();
|
||||
let ops = [];
|
||||
work.then(() => {
|
||||
ops.push('inside callback');
|
||||
});
|
||||
expect(ops).toEqual(['inside callback']);
|
||||
});
|
||||
|
||||
it('supports hydration', async () => {
|
||||
const markup = await new Promise(resolve =>
|
||||
resolve(
|
||||
@@ -129,200 +100,6 @@ describe('ReactDOMRoot', () => {
|
||||
expect(container.textContent).toEqual('abdc');
|
||||
});
|
||||
|
||||
it('can defer a commit by batching it', () => {
|
||||
const root = ReactDOM.unstable_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('applies setState in componentDidMount synchronously in a batch', done => {
|
||||
class App extends React.Component {
|
||||
state = {mounted: false};
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
mounted: true,
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return this.state.mounted ? 'Hi' : 'Bye';
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const batch = root.createBatch();
|
||||
batch.render(<App />);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
// 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(['', 'Hi']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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.unstable_createRoot(container);
|
||||
const batch = root.createBatch();
|
||||
batch.render(<Foo>Hi</Foo>);
|
||||
// Flush all async work.
|
||||
Scheduler.unstable_flushAll();
|
||||
// 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 root = ReactDOM.unstable_createRoot(container);
|
||||
const batch = root.createBatch();
|
||||
batch.render('Foo');
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
// 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.unstable_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.unstable_createRoot(container);
|
||||
root.render(1);
|
||||
|
||||
Scheduler.unstable_advanceTime(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('');
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('1');
|
||||
});
|
||||
|
||||
it('two batches created simultaneously are committed separately', () => {
|
||||
// (In other words, they have distinct expiration times)
|
||||
const root = ReactDOM.unstable_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.unstable_createRoot(container);
|
||||
const batch1 = root.createBatch();
|
||||
batch1.render(1);
|
||||
|
||||
// This batch has a later expiration time
|
||||
Scheduler.unstable_advanceTime(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 committing an earlier batch', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const batch1 = root.createBatch();
|
||||
batch1.render(1);
|
||||
|
||||
// This batch has a later expiration time
|
||||
Scheduler.unstable_advanceTime(2000);
|
||||
const batch2 = root.createBatch();
|
||||
batch2.render(2);
|
||||
|
||||
expect(container.textContent).toEqual('');
|
||||
|
||||
batch2.commit();
|
||||
expect(container.textContent).toEqual('2');
|
||||
|
||||
batch1.commit();
|
||||
Scheduler.unstable_flushAll();
|
||||
expect(container.textContent).toEqual('1');
|
||||
});
|
||||
|
||||
it('handles fatal errors triggered by batch.commit()', () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
const batch = root.createBatch();
|
||||
const InvalidType = undefined;
|
||||
expect(() => batch.render(<InvalidType />)).toWarnDev(
|
||||
['React.createElement: type is invalid'],
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(() => batch.commit()).toThrow('Element type is invalid');
|
||||
});
|
||||
|
||||
it('throws a good message on invalid containers', () => {
|
||||
expect(() => {
|
||||
ReactDOM.unstable_createRoot(<div>Hi</div>);
|
||||
|
||||
260
packages/react-dom/src/client/ReactDOM.js
vendored
260
packages/react-dom/src/client/ReactDOM.js
vendored
@@ -11,19 +11,13 @@ import type {ReactNodeList} from 'shared/ReactTypes';
|
||||
import type {RootTag} from 'shared/ReactRootTags';
|
||||
// TODO: This type is shared between the reconciler and ReactDOM, but will
|
||||
// eventually be lifted out to the renderer.
|
||||
import type {
|
||||
FiberRoot,
|
||||
Batch as FiberRootBatch,
|
||||
} from 'react-reconciler/src/ReactFiberRoot';
|
||||
import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
|
||||
|
||||
import '../shared/checkReact';
|
||||
import './ReactDOMClientInjection';
|
||||
|
||||
import {
|
||||
computeUniqueAsyncExpiration,
|
||||
findHostInstanceWithNoPortals,
|
||||
updateContainerAtExpirationTime,
|
||||
flushRoot,
|
||||
createContainer,
|
||||
updateContainer,
|
||||
batchedEventUpdates,
|
||||
@@ -179,209 +173,21 @@ setRestoreImplementation(restoreControlledState);
|
||||
|
||||
export type DOMContainer =
|
||||
| (Element & {
|
||||
_reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
|
||||
_reactRootContainer: ?_ReactRoot,
|
||||
_reactHasBeenPassedToCreateRootDEV: ?boolean,
|
||||
})
|
||||
| (Document & {
|
||||
_reactRootContainer: ?(_ReactRoot | _ReactSyncRoot),
|
||||
_reactRootContainer: ?_ReactRoot,
|
||||
_reactHasBeenPassedToCreateRootDEV: ?boolean,
|
||||
});
|
||||
|
||||
type Batch = FiberRootBatch & {
|
||||
render(children: ReactNodeList): Work,
|
||||
then(onComplete: () => mixed): void,
|
||||
commit(): void,
|
||||
|
||||
// The ReactRoot constructor is hoisted but the prototype methods are not. If
|
||||
// we move ReactRoot to be above ReactBatch, the inverse error occurs.
|
||||
// $FlowFixMe Hoisting issue.
|
||||
_root: _ReactRoot | _ReactSyncRoot,
|
||||
_hasChildren: boolean,
|
||||
_children: ReactNodeList,
|
||||
|
||||
_callbacks: Array<() => mixed> | null,
|
||||
_didComplete: boolean,
|
||||
};
|
||||
|
||||
type _ReactSyncRoot = {
|
||||
render(children: ReactNodeList, callback: ?() => mixed): Work,
|
||||
unmount(callback: ?() => mixed): Work,
|
||||
type _ReactRoot = {
|
||||
render(children: ReactNodeList, callback: ?() => mixed): void,
|
||||
unmount(callback: ?() => mixed): void,
|
||||
|
||||
_internalRoot: FiberRoot,
|
||||
};
|
||||
|
||||
type _ReactRoot = _ReactSyncRoot & {
|
||||
createBatch(): Batch,
|
||||
};
|
||||
|
||||
function ReactBatch(root: _ReactRoot | _ReactSyncRoot) {
|
||||
const expirationTime = computeUniqueAsyncExpiration();
|
||||
this._expirationTime = expirationTime;
|
||||
this._root = root;
|
||||
this._next = null;
|
||||
this._callbacks = null;
|
||||
this._didComplete = false;
|
||||
this._hasChildren = false;
|
||||
this._children = null;
|
||||
this._defer = true;
|
||||
}
|
||||
ReactBatch.prototype.render = function(children: ReactNodeList) {
|
||||
invariant(
|
||||
this._defer,
|
||||
'batch.render: Cannot render a batch that already committed.',
|
||||
);
|
||||
this._hasChildren = true;
|
||||
this._children = children;
|
||||
const internalRoot = this._root._internalRoot;
|
||||
const expirationTime = this._expirationTime;
|
||||
const work = new ReactWork();
|
||||
updateContainerAtExpirationTime(
|
||||
children,
|
||||
internalRoot,
|
||||
null,
|
||||
expirationTime,
|
||||
null,
|
||||
work._onCommit,
|
||||
);
|
||||
return work;
|
||||
};
|
||||
ReactBatch.prototype.then = function(onComplete: () => mixed) {
|
||||
if (this._didComplete) {
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
let callbacks = this._callbacks;
|
||||
if (callbacks === null) {
|
||||
callbacks = this._callbacks = [];
|
||||
}
|
||||
callbacks.push(onComplete);
|
||||
};
|
||||
ReactBatch.prototype.commit = function() {
|
||||
const internalRoot = this._root._internalRoot;
|
||||
let firstBatch = internalRoot.firstBatch;
|
||||
invariant(
|
||||
this._defer && firstBatch !== null,
|
||||
'batch.commit: Cannot commit a batch multiple times.',
|
||||
);
|
||||
|
||||
if (!this._hasChildren) {
|
||||
// This batch is empty. Return.
|
||||
this._next = null;
|
||||
this._defer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let expirationTime = this._expirationTime;
|
||||
|
||||
// Ensure this is the first batch in the list.
|
||||
if (firstBatch !== this) {
|
||||
// This batch is not the earliest batch. We need to move it to the front.
|
||||
// Update its expiration time to be the expiration time of the earliest
|
||||
// batch, so that we can flush it without flushing the other batches.
|
||||
if (this._hasChildren) {
|
||||
expirationTime = this._expirationTime = firstBatch._expirationTime;
|
||||
// Rendering this batch again ensures its children will be the final state
|
||||
// when we flush (updates are processed in insertion order: last
|
||||
// update wins).
|
||||
// TODO: This forces a restart. Should we print a warning?
|
||||
this.render(this._children);
|
||||
}
|
||||
|
||||
// Remove the batch from the list.
|
||||
let previous = null;
|
||||
let batch = firstBatch;
|
||||
while (batch !== this) {
|
||||
previous = batch;
|
||||
batch = batch._next;
|
||||
}
|
||||
invariant(
|
||||
previous !== null,
|
||||
'batch.commit: Cannot commit a batch multiple times.',
|
||||
);
|
||||
previous._next = batch._next;
|
||||
|
||||
// Add it to the front.
|
||||
this._next = firstBatch;
|
||||
firstBatch = internalRoot.firstBatch = this;
|
||||
}
|
||||
|
||||
// Synchronously flush all the work up to this batch's expiration time.
|
||||
this._defer = false;
|
||||
flushRoot(internalRoot, expirationTime);
|
||||
|
||||
// Pop the batch from the list.
|
||||
const next = this._next;
|
||||
this._next = null;
|
||||
firstBatch = internalRoot.firstBatch = next;
|
||||
|
||||
// Append the next earliest batch's children to the update queue.
|
||||
if (firstBatch !== null && firstBatch._hasChildren) {
|
||||
firstBatch.render(firstBatch._children);
|
||||
}
|
||||
};
|
||||
ReactBatch.prototype._onComplete = function() {
|
||||
if (this._didComplete) {
|
||||
return;
|
||||
}
|
||||
this._didComplete = true;
|
||||
const callbacks = this._callbacks;
|
||||
if (callbacks === null) {
|
||||
return;
|
||||
}
|
||||
// TODO: Error handling.
|
||||
for (let i = 0; i < callbacks.length; i++) {
|
||||
const callback = callbacks[i];
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
type Work = {
|
||||
then(onCommit: () => mixed): void,
|
||||
_onCommit: () => void,
|
||||
_callbacks: Array<() => mixed> | null,
|
||||
_didCommit: boolean,
|
||||
};
|
||||
|
||||
function ReactWork() {
|
||||
this._callbacks = null;
|
||||
this._didCommit = false;
|
||||
// TODO: Avoid need to bind by replacing callbacks in the update queue with
|
||||
// list of Work objects.
|
||||
this._onCommit = this._onCommit.bind(this);
|
||||
}
|
||||
ReactWork.prototype.then = function(onCommit: () => mixed): void {
|
||||
if (this._didCommit) {
|
||||
onCommit();
|
||||
return;
|
||||
}
|
||||
let callbacks = this._callbacks;
|
||||
if (callbacks === null) {
|
||||
callbacks = this._callbacks = [];
|
||||
}
|
||||
callbacks.push(onCommit);
|
||||
};
|
||||
ReactWork.prototype._onCommit = function(): void {
|
||||
if (this._didCommit) {
|
||||
return;
|
||||
}
|
||||
this._didCommit = true;
|
||||
const callbacks = this._callbacks;
|
||||
if (callbacks === null) {
|
||||
return;
|
||||
}
|
||||
// TODO: Error handling.
|
||||
for (let i = 0; i < callbacks.length; i++) {
|
||||
const callback = callbacks[i];
|
||||
invariant(
|
||||
typeof callback === 'function',
|
||||
'Invalid argument passed as callback. Expected a function. Instead ' +
|
||||
'received: %s',
|
||||
callback,
|
||||
);
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
function createRootImpl(
|
||||
container: DOMContainer,
|
||||
tag: RootTag,
|
||||
@@ -418,64 +224,24 @@ function ReactRoot(container: DOMContainer, options: void | RootOptions) {
|
||||
ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
|
||||
children: ReactNodeList,
|
||||
callback: ?() => mixed,
|
||||
): Work {
|
||||
): void {
|
||||
const root = this._internalRoot;
|
||||
const work = new ReactWork();
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (__DEV__) {
|
||||
warnOnInvalidCallback(callback, 'render');
|
||||
}
|
||||
if (callback !== null) {
|
||||
work.then(callback);
|
||||
}
|
||||
updateContainer(children, root, null, work._onCommit);
|
||||
return work;
|
||||
updateContainer(children, root, null, callback);
|
||||
};
|
||||
|
||||
ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
|
||||
callback: ?() => mixed,
|
||||
): Work {
|
||||
): void {
|
||||
const root = this._internalRoot;
|
||||
const work = new ReactWork();
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (__DEV__) {
|
||||
warnOnInvalidCallback(callback, 'render');
|
||||
}
|
||||
if (callback !== null) {
|
||||
work.then(callback);
|
||||
}
|
||||
updateContainer(null, root, null, work._onCommit);
|
||||
return work;
|
||||
};
|
||||
|
||||
// Sync roots cannot create batches. Only concurrent ones.
|
||||
ReactRoot.prototype.createBatch = function(): Batch {
|
||||
const batch = new ReactBatch(this);
|
||||
const expirationTime = batch._expirationTime;
|
||||
|
||||
const internalRoot = this._internalRoot;
|
||||
const firstBatch = internalRoot.firstBatch;
|
||||
if (firstBatch === null) {
|
||||
internalRoot.firstBatch = batch;
|
||||
batch._next = null;
|
||||
} else {
|
||||
// Insert sorted by expiration time then insertion order
|
||||
let insertAfter = null;
|
||||
let insertBefore = firstBatch;
|
||||
while (
|
||||
insertBefore !== null &&
|
||||
insertBefore._expirationTime >= expirationTime
|
||||
) {
|
||||
insertAfter = insertBefore;
|
||||
insertBefore = insertBefore._next;
|
||||
}
|
||||
batch._next = insertBefore;
|
||||
if (insertAfter !== null) {
|
||||
insertAfter._next = batch;
|
||||
}
|
||||
}
|
||||
|
||||
return batch;
|
||||
updateContainer(null, root, null, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -529,7 +295,7 @@ let warnedAboutHydrateAPI = false;
|
||||
function legacyCreateRootFromDOMContainer(
|
||||
container: DOMContainer,
|
||||
forceHydrate: boolean,
|
||||
): _ReactSyncRoot {
|
||||
): _ReactRoot {
|
||||
const shouldHydrate =
|
||||
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
|
||||
// First clear any existing content.
|
||||
@@ -593,7 +359,7 @@ function legacyRenderSubtreeIntoContainer(
|
||||
|
||||
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
|
||||
// member of intersection type." Whyyyyyy.
|
||||
let root: _ReactSyncRoot = (container._reactRootContainer: any);
|
||||
let root: _ReactRoot = (container._reactRootContainer: any);
|
||||
let fiberRoot;
|
||||
if (!root) {
|
||||
// Initial mount
|
||||
@@ -899,7 +665,7 @@ function createRoot(
|
||||
function createSyncRoot(
|
||||
container: DOMContainer,
|
||||
options?: RootOptions,
|
||||
): _ReactSyncRoot {
|
||||
): _ReactRoot {
|
||||
const functionName = enableStableConcurrentModeAPIs
|
||||
? 'createRoot'
|
||||
: 'unstable_createRoot';
|
||||
|
||||
@@ -857,7 +857,7 @@ describe('DOMEventResponderSystem', () => {
|
||||
|
||||
function Test({counter}) {
|
||||
const listener = React.unstable_useResponder(TestResponder, {counter});
|
||||
|
||||
Scheduler.unstable_yieldValue('Test');
|
||||
return (
|
||||
<button listeners={listener} ref={ref}>
|
||||
Press me
|
||||
@@ -866,11 +866,8 @@ describe('DOMEventResponderSystem', () => {
|
||||
}
|
||||
|
||||
let root = ReactDOM.unstable_createRoot(container);
|
||||
let batch = root.createBatch();
|
||||
batch.render(<Test counter={0} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
batch.commit();
|
||||
root.render(<Test counter={0} />);
|
||||
expect(Scheduler).toFlushAndYield(['Test']);
|
||||
|
||||
// Click the button
|
||||
dispatchClickEvent(ref.current);
|
||||
@@ -880,10 +877,9 @@ describe('DOMEventResponderSystem', () => {
|
||||
log.length = 0;
|
||||
|
||||
// Increase counter
|
||||
batch = root.createBatch();
|
||||
batch.render(<Test counter={1} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.runAllTimers();
|
||||
root.render(<Test counter={1} />);
|
||||
// Yield before committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Test']);
|
||||
|
||||
// Click the button again
|
||||
dispatchClickEvent(ref.current);
|
||||
@@ -893,7 +889,7 @@ describe('DOMEventResponderSystem', () => {
|
||||
log.length = 0;
|
||||
|
||||
// Commit
|
||||
batch.commit();
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
dispatchClickEvent(ref.current);
|
||||
expect(log).toEqual([{counter: 1}]);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import type {RootTag} from 'shared/ReactRootTags';
|
||||
|
||||
import * as Scheduler from 'scheduler/unstable_mock';
|
||||
import {createPortal} from 'shared/ReactPortal';
|
||||
import expect from 'expect';
|
||||
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
||||
import enqueueTask from 'shared/enqueueTask';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
@@ -1198,31 +1197,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
console.log(...bufferedLog);
|
||||
},
|
||||
|
||||
flushWithoutCommitting(
|
||||
expectedFlush: Array<mixed>,
|
||||
rootID: string = DEFAULT_ROOT_ID,
|
||||
) {
|
||||
const root: any = roots.get(rootID);
|
||||
const expiration = NoopRenderer.computeUniqueAsyncExpiration();
|
||||
const batch = {
|
||||
_defer: true,
|
||||
_expirationTime: expiration,
|
||||
_onComplete: () => {
|
||||
root.firstBatch = null;
|
||||
},
|
||||
_next: null,
|
||||
};
|
||||
root.firstBatch = batch;
|
||||
Scheduler.unstable_flushAllWithoutAsserting();
|
||||
const actual = Scheduler.unstable_clearYields();
|
||||
expect(actual).toEqual(expectedFlush);
|
||||
return (expectedCommit: Array<mixed>) => {
|
||||
batch._defer = false;
|
||||
NoopRenderer.flushRoot(root, expiration);
|
||||
expect(Scheduler.unstable_clearYields()).toEqual(expectedCommit);
|
||||
};
|
||||
},
|
||||
|
||||
getRoot(rootID: string = DEFAULT_ROOT_ID) {
|
||||
return roots.get(rootID);
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
scheduleWork,
|
||||
flushPassiveEffects,
|
||||
} from './ReactFiberWorkLoop';
|
||||
import {updateContainerAtExpirationTime} from './ReactFiberReconciler';
|
||||
import {updateContainer, syncUpdates} from './ReactFiberReconciler';
|
||||
import {emptyContextObject} from './ReactFiberContext';
|
||||
import {Sync} from './ReactFiberExpirationTime';
|
||||
import {
|
||||
@@ -258,7 +258,9 @@ export let scheduleRoot: ScheduleRoot = (
|
||||
return;
|
||||
}
|
||||
flushPassiveEffects();
|
||||
updateContainerAtExpirationTime(element, root, null, Sync, null);
|
||||
syncUpdates(() => {
|
||||
updateContainer(element, root, null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {
|
||||
import {FundamentalComponent} from 'shared/ReactWorkTags';
|
||||
import type {ReactNodeList} from 'shared/ReactTypes';
|
||||
import type {ExpirationTime} from './ReactFiberExpirationTime';
|
||||
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
|
||||
import type {
|
||||
SuspenseHydrationCallbacks,
|
||||
SuspenseState,
|
||||
@@ -51,7 +50,6 @@ import {
|
||||
import {createFiberRoot} from './ReactFiberRoot';
|
||||
import {injectInternals} from './ReactFiberDevToolsHook';
|
||||
import {
|
||||
computeUniqueAsyncExpiration,
|
||||
requestCurrentTime,
|
||||
computeExpirationForFiber,
|
||||
scheduleWork,
|
||||
@@ -138,92 +136,6 @@ function getContextForSubtree(
|
||||
return parentContext;
|
||||
}
|
||||
|
||||
function scheduleRootUpdate(
|
||||
current: Fiber,
|
||||
element: ReactNodeList,
|
||||
expirationTime: ExpirationTime,
|
||||
suspenseConfig: null | SuspenseConfig,
|
||||
callback: ?Function,
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if (
|
||||
ReactCurrentFiberPhase === 'render' &&
|
||||
ReactCurrentFiberCurrent !== null &&
|
||||
!didWarnAboutNestedUpdates
|
||||
) {
|
||||
didWarnAboutNestedUpdates = true;
|
||||
warningWithoutStack(
|
||||
false,
|
||||
'Render methods should be a pure function of props and state; ' +
|
||||
'triggering nested component updates from render is not allowed. ' +
|
||||
'If necessary, trigger nested updates in componentDidUpdate.\n\n' +
|
||||
'Check the render method of %s.',
|
||||
getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const update = createUpdate(expirationTime, suspenseConfig);
|
||||
// Caution: React DevTools currently depends on this property
|
||||
// being called "element".
|
||||
update.payload = {element};
|
||||
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (callback !== null) {
|
||||
warningWithoutStack(
|
||||
typeof callback === 'function',
|
||||
'render(...): Expected the last optional `callback` argument to be a ' +
|
||||
'function. Instead received: %s.',
|
||||
callback,
|
||||
);
|
||||
update.callback = callback;
|
||||
}
|
||||
|
||||
enqueueUpdate(current, update);
|
||||
scheduleWork(current, expirationTime);
|
||||
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
export function updateContainerAtExpirationTime(
|
||||
element: ReactNodeList,
|
||||
container: OpaqueRoot,
|
||||
parentComponent: ?React$Component<any, any>,
|
||||
expirationTime: ExpirationTime,
|
||||
suspenseConfig: null | SuspenseConfig,
|
||||
callback: ?Function,
|
||||
) {
|
||||
// TODO: If this is a nested container, this won't be the root.
|
||||
const current = container.current;
|
||||
|
||||
if (__DEV__) {
|
||||
if (ReactFiberInstrumentation.debugTool) {
|
||||
if (current.alternate === null) {
|
||||
ReactFiberInstrumentation.debugTool.onMountContainer(container);
|
||||
} else if (element === null) {
|
||||
ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
|
||||
} else {
|
||||
ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context = getContextForSubtree(parentComponent);
|
||||
if (container.context === null) {
|
||||
container.context = context;
|
||||
} else {
|
||||
container.pendingContext = context;
|
||||
}
|
||||
|
||||
return scheduleRootUpdate(
|
||||
current,
|
||||
element,
|
||||
expirationTime,
|
||||
suspenseConfig,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
function findHostInstance(component: Object): PublicInstance | null {
|
||||
const fiber = getInstance(component);
|
||||
if (fiber === undefined) {
|
||||
@@ -333,19 +245,68 @@ export function updateContainer(
|
||||
current,
|
||||
suspenseConfig,
|
||||
);
|
||||
return updateContainerAtExpirationTime(
|
||||
element,
|
||||
container,
|
||||
parentComponent,
|
||||
expirationTime,
|
||||
suspenseConfig,
|
||||
callback,
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
if (ReactFiberInstrumentation.debugTool) {
|
||||
if (current.alternate === null) {
|
||||
ReactFiberInstrumentation.debugTool.onMountContainer(container);
|
||||
} else if (element === null) {
|
||||
ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
|
||||
} else {
|
||||
ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context = getContextForSubtree(parentComponent);
|
||||
if (container.context === null) {
|
||||
container.context = context;
|
||||
} else {
|
||||
container.pendingContext = context;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
if (
|
||||
ReactCurrentFiberPhase === 'render' &&
|
||||
ReactCurrentFiberCurrent !== null &&
|
||||
!didWarnAboutNestedUpdates
|
||||
) {
|
||||
didWarnAboutNestedUpdates = true;
|
||||
warningWithoutStack(
|
||||
false,
|
||||
'Render methods should be a pure function of props and state; ' +
|
||||
'triggering nested component updates from render is not allowed. ' +
|
||||
'If necessary, trigger nested updates in componentDidUpdate.\n\n' +
|
||||
'Check the render method of %s.',
|
||||
getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const update = createUpdate(expirationTime, suspenseConfig);
|
||||
// Caution: React DevTools currently depends on this property
|
||||
// being called "element".
|
||||
update.payload = {element};
|
||||
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (callback !== null) {
|
||||
warningWithoutStack(
|
||||
typeof callback === 'function',
|
||||
'render(...): Expected the last optional `callback` argument to be a ' +
|
||||
'function. Instead received: %s.',
|
||||
callback,
|
||||
);
|
||||
update.callback = callback;
|
||||
}
|
||||
|
||||
enqueueUpdate(current, update);
|
||||
scheduleWork(current, expirationTime);
|
||||
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
export {
|
||||
flushRoot,
|
||||
computeUniqueAsyncExpiration,
|
||||
batchedEventUpdates,
|
||||
batchedUpdates,
|
||||
unbatchedUpdates,
|
||||
|
||||
13
packages/react-reconciler/src/ReactFiberRoot.js
vendored
13
packages/react-reconciler/src/ReactFiberRoot.js
vendored
@@ -26,14 +26,6 @@ import {
|
||||
import {unstable_getThreadID} from 'scheduler/tracing';
|
||||
import {NoPriority} from './SchedulerWithReactIntegration';
|
||||
|
||||
// TODO: This should be lifted into the renderer.
|
||||
export type Batch = {
|
||||
_defer: boolean,
|
||||
_expirationTime: ExpirationTime,
|
||||
_onComplete: () => mixed,
|
||||
_next: Batch | null,
|
||||
};
|
||||
|
||||
export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
|
||||
|
||||
type BaseFiberRootProperties = {|
|
||||
@@ -63,10 +55,6 @@ type BaseFiberRootProperties = {|
|
||||
pendingContext: Object | null,
|
||||
// Determines if we should attempt to hydrate on the initial mount
|
||||
+hydrate: boolean,
|
||||
// List of top-level batches. This list indicates whether a commit should be
|
||||
// deferred. Also contains completion callbacks.
|
||||
// TODO: Lift this into the renderer
|
||||
firstBatch: Batch | null,
|
||||
// Node returned by Scheduler.scheduleCallback
|
||||
callbackNode: *,
|
||||
// Expiration of the callback associated with this root
|
||||
@@ -125,7 +113,6 @@ function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.context = null;
|
||||
this.pendingContext = null;
|
||||
this.hydrate = hydrate;
|
||||
this.firstBatch = null;
|
||||
this.callbackNode = null;
|
||||
this.callbackPriority = NoPriority;
|
||||
this.firstPendingTime = NoWork;
|
||||
|
||||
@@ -200,14 +200,13 @@ const LegacyUnbatchedContext = /* */ 0b001000;
|
||||
const RenderContext = /* */ 0b010000;
|
||||
const CommitContext = /* */ 0b100000;
|
||||
|
||||
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
|
||||
const RootIncomplete = 0;
|
||||
const RootFatalErrored = 1;
|
||||
const RootErrored = 2;
|
||||
const RootSuspended = 3;
|
||||
const RootSuspendedWithDelay = 4;
|
||||
const RootCompleted = 5;
|
||||
const RootLocked = 6;
|
||||
|
||||
export type Thenable = {
|
||||
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
|
||||
@@ -719,7 +718,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
|
||||
const finishedWork: Fiber = ((root.finishedWork =
|
||||
root.current.alternate): any);
|
||||
root.finishedExpirationTime = expirationTime;
|
||||
resolveLocksOnRoot(root, expirationTime);
|
||||
finishConcurrentRender(
|
||||
root,
|
||||
finishedWork,
|
||||
@@ -976,13 +974,6 @@ function finishConcurrentRender(
|
||||
commitRoot(root);
|
||||
break;
|
||||
}
|
||||
case RootLocked: {
|
||||
// This root has a lock that prevents it from committing. Exit. If
|
||||
// we begin work on the root again, without any intervening updates,
|
||||
// it will finish without doing additional work.
|
||||
markRootSuspendedAtTime(root, expirationTime);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
invariant(false, 'Unknown root exit status.');
|
||||
}
|
||||
@@ -1060,12 +1051,10 @@ function performSyncWorkOnRoot(root) {
|
||||
);
|
||||
} else {
|
||||
// We now have a consistent tree. Because this is a sync render, we
|
||||
// will commit it even if something suspended. The only exception is
|
||||
// if the root is locked (using the unstable_createBatch API).
|
||||
// will commit it even if something suspended.
|
||||
stopFinishedWorkLoopTimer();
|
||||
root.finishedWork = (root.current.alternate: any);
|
||||
root.finishedExpirationTime = expirationTime;
|
||||
resolveLocksOnRoot(root, expirationTime);
|
||||
finishSyncRender(root, workInProgressRootExitStatus, expirationTime);
|
||||
}
|
||||
|
||||
@@ -1079,25 +1068,15 @@ function performSyncWorkOnRoot(root) {
|
||||
}
|
||||
|
||||
function finishSyncRender(root, exitStatus, expirationTime) {
|
||||
if (exitStatus === RootLocked) {
|
||||
// This root has a lock that prevents it from committing. Exit. If we
|
||||
// begin work on the root again, without any intervening updates, it
|
||||
// will finish without doing additional work.
|
||||
markRootSuspendedAtTime(root, expirationTime);
|
||||
} else {
|
||||
// Set this to null to indicate there's no in-progress render.
|
||||
workInProgressRoot = null;
|
||||
// Set this to null to indicate there's no in-progress render.
|
||||
workInProgressRoot = null;
|
||||
|
||||
if (__DEV__) {
|
||||
if (
|
||||
exitStatus === RootSuspended ||
|
||||
exitStatus === RootSuspendedWithDelay
|
||||
) {
|
||||
flushSuspensePriorityWarningInDEV();
|
||||
}
|
||||
if (__DEV__) {
|
||||
if (exitStatus === RootSuspended || exitStatus === RootSuspendedWithDelay) {
|
||||
flushSuspensePriorityWarningInDEV();
|
||||
}
|
||||
commitRoot(root);
|
||||
}
|
||||
commitRoot(root);
|
||||
}
|
||||
|
||||
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
|
||||
@@ -1140,21 +1119,6 @@ export function flushDiscreteUpdates() {
|
||||
flushPassiveEffects();
|
||||
}
|
||||
|
||||
function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
|
||||
const firstBatch = root.firstBatch;
|
||||
if (
|
||||
firstBatch !== null &&
|
||||
firstBatch._defer &&
|
||||
firstBatch._expirationTime >= expirationTime
|
||||
) {
|
||||
scheduleCallback(NormalPriority, () => {
|
||||
firstBatch._onComplete();
|
||||
return null;
|
||||
});
|
||||
workInProgressRootExitStatus = RootLocked;
|
||||
}
|
||||
}
|
||||
|
||||
export function deferredUpdates<A>(fn: () => A): A {
|
||||
// TODO: Remove in favor of Scheduler.next
|
||||
return runWithPriority(NormalPriority, fn);
|
||||
|
||||
@@ -97,15 +97,6 @@ function loadModules({
|
||||
};
|
||||
}
|
||||
|
||||
const mockDevToolsForTest = () => {
|
||||
jest.mock('react-reconciler/src/ReactFiberDevToolsHook', () => ({
|
||||
injectInternals: () => {},
|
||||
onCommitRoot: () => {},
|
||||
onCommitUnmount: () => {},
|
||||
isDevToolsPresent: true,
|
||||
}));
|
||||
};
|
||||
|
||||
describe('Profiler', () => {
|
||||
describe('works in profiling and non-profiling bundles', () => {
|
||||
[true, false].forEach(enableSchedulerTracing => {
|
||||
@@ -1205,71 +1196,6 @@ describe('Profiler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle interleaved async yields and batched commits', () => {
|
||||
jest.resetModules();
|
||||
mockDevToolsForTest();
|
||||
loadModules({useNoopRenderer: true});
|
||||
|
||||
const Child = ({duration, id}) => {
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
Scheduler.unstable_yieldValue(`Child:render:${id}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
class Parent extends React.Component {
|
||||
componentDidMount() {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`Parent:componentDidMount:${this.props.id}`,
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const {duration, id} = this.props;
|
||||
return (
|
||||
<>
|
||||
<Child duration={duration} id={id} />
|
||||
<Child duration={duration} id={id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Scheduler.unstable_advanceTime(50);
|
||||
|
||||
ReactNoop.renderToRootWithID(<Parent duration={3} id="one" />, 'one');
|
||||
|
||||
// Process up to the <Parent> component, but yield before committing.
|
||||
// This ensures that the profiler timer still has paused fibers.
|
||||
const commitFirstRender = ReactNoop.flushWithoutCommitting(
|
||||
['Child:render:one', 'Child:render:one'],
|
||||
'one',
|
||||
);
|
||||
|
||||
expect(ReactNoop.getRoot('one').current.actualDuration).toBe(0);
|
||||
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
|
||||
// Process some async work, but yield before committing it.
|
||||
ReactNoop.renderToRootWithID(<Parent duration={7} id="two" />, 'two');
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Child:render:two']);
|
||||
|
||||
Scheduler.unstable_advanceTime(150);
|
||||
|
||||
// Commit the previously paused, batched work.
|
||||
commitFirstRender(['Parent:componentDidMount:one']);
|
||||
|
||||
expect(ReactNoop.getRoot('one').current.actualDuration).toBe(6);
|
||||
expect(ReactNoop.getRoot('two').current.actualDuration).toBe(0);
|
||||
|
||||
Scheduler.unstable_advanceTime(200);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Child:render:two',
|
||||
'Parent:componentDidMount:two',
|
||||
]);
|
||||
|
||||
expect(ReactNoop.getRoot('two').current.actualDuration).toBe(14);
|
||||
});
|
||||
|
||||
describe('interaction tracing', () => {
|
||||
let onInteractionScheduledWorkCompleted;
|
||||
let onInteractionTraced;
|
||||
|
||||
@@ -14,7 +14,6 @@ let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
let SchedulerTracing;
|
||||
let Scheduler;
|
||||
let ReactCache;
|
||||
|
||||
function loadModules() {
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
@@ -27,12 +26,9 @@ function loadModules() {
|
||||
SchedulerTracing = require('scheduler/tracing');
|
||||
ReactDOM = require('react-dom');
|
||||
Scheduler = require('scheduler');
|
||||
ReactCache = require('react-cache');
|
||||
}
|
||||
|
||||
describe('ProfilerDOM', () => {
|
||||
let TextResource;
|
||||
let resourcePromise;
|
||||
let onInteractionScheduledWorkCompleted;
|
||||
let onInteractionTraced;
|
||||
|
||||
@@ -51,97 +47,83 @@ describe('ProfilerDOM', () => {
|
||||
onWorkStarted: () => {},
|
||||
onWorkStopped: () => {},
|
||||
});
|
||||
|
||||
resourcePromise = null;
|
||||
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
resourcePromise = new Promise(
|
||||
SchedulerTracing.unstable_wrap((resolve, reject) => {
|
||||
setTimeout(
|
||||
SchedulerTracing.unstable_wrap(() => {
|
||||
resolve(text);
|
||||
}),
|
||||
ms,
|
||||
);
|
||||
}),
|
||||
);
|
||||
return resourcePromise;
|
||||
}, ([text, ms]) => text);
|
||||
});
|
||||
|
||||
const AsyncText = ({ms, text}) => {
|
||||
TextResource.read([text, ms]);
|
||||
return text;
|
||||
};
|
||||
function Text(props) {
|
||||
Scheduler.unstable_yieldValue(props.text);
|
||||
return props.text;
|
||||
}
|
||||
|
||||
const Text = ({text}) => text;
|
||||
it('should correctly trace interactions for async roots', async () => {
|
||||
let resolve;
|
||||
let thenable = {
|
||||
then(res) {
|
||||
resolve = () => {
|
||||
thenable = null;
|
||||
res();
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
it('should correctly trace interactions for async roots', async done => {
|
||||
let batch, element, interaction;
|
||||
function Async() {
|
||||
if (thenable !== null) {
|
||||
Scheduler.unstable_yieldValue('Suspend! [Async]');
|
||||
throw thenable;
|
||||
}
|
||||
Scheduler.unstable_yieldValue('Async');
|
||||
return 'Async';
|
||||
}
|
||||
|
||||
const element = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(element);
|
||||
|
||||
let interaction;
|
||||
let wrappedResolve;
|
||||
SchedulerTracing.unstable_trace('initial_event', performance.now(), () => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
interaction = Array.from(interactions)[0];
|
||||
|
||||
element = document.createElement('div');
|
||||
const root = ReactDOM.unstable_createRoot(element);
|
||||
batch = root.createBatch();
|
||||
batch.render(
|
||||
root.render(
|
||||
<React.Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Text" ms={2000} />
|
||||
<Async />
|
||||
</React.Suspense>,
|
||||
);
|
||||
batch.then(
|
||||
SchedulerTracing.unstable_wrap(() => {
|
||||
batch.commit();
|
||||
|
||||
expect(element.textContent).toBe('Loading...');
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
resourcePromise.then(
|
||||
SchedulerTracing.unstable_wrap(() => {
|
||||
jest.runAllTimers();
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
expect(element.textContent).toBe('Text');
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
// Evaluate in an unwrapped callback,
|
||||
// Because trace/wrap won't decrement the count within the wrapped callback.
|
||||
Promise.resolve().then(() => {
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(interaction);
|
||||
|
||||
expect(interaction.__count).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
wrappedResolve = SchedulerTracing.unstable_wrap(() => resolve());
|
||||
});
|
||||
|
||||
// Render, suspend, and commit fallback
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
|
||||
expect(element.textContent).toEqual('Loading...');
|
||||
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
|
||||
interaction,
|
||||
);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
Scheduler.unstable_flushAll();
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
// Ping React to try rendering again
|
||||
wrappedResolve();
|
||||
|
||||
// Complete the tree without committing it
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Async']);
|
||||
// Still showing the fallback
|
||||
expect(element.textContent).toEqual('Loading...');
|
||||
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
|
||||
interaction,
|
||||
);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
expect(element.textContent).toEqual('Async');
|
||||
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
|
||||
interaction,
|
||||
);
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user