Restart from root if promise pings before end of render phase (#13774)

* Restart from root if promise pings before end of render phase

* Test that placeholder resolves successfully even if fallback render is pending
This commit is contained in:
Andrew Clark
2018-10-04 12:55:52 -07:00
committed by GitHub
parent cbc2240288
commit acc7f404ce
3 changed files with 78 additions and 1 deletions
+9 -1
View File
@@ -111,7 +111,7 @@ import {
computeAsyncExpiration,
computeInteractiveExpiration,
} from './ReactFiberExpirationTime';
import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode';
import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
import {createCapturedValue} from './ReactCapturedValue';
import {
@@ -1597,6 +1597,14 @@ function retrySuspendedRoot(
markPendingPriorityLevel(root, retryTime);
}
if ((fiber.mode & ConcurrentMode) !== NoContext) {
if (root === nextRoot && nextRenderExpirationTime === suspendedTime) {
// Received a ping at the same priority level at which we're currently
// rendering. Restart from the root.
nextRoot = null;
}
}
scheduleWorkToRoot(fiber, retryTime);
const rootExpirationTime = root.expirationTime;
if (rootExpirationTime !== NoWork) {
@@ -189,4 +189,57 @@ describe('ReactSuspense', () => {
expect(root).toFlushAndYield(['B']);
expect(root).toMatchRenderedOutput('AB');
});
it('interrupts current render if promise resolves before current render phase', () => {
let didResolve = false;
let listeners = [];
const thenable = {
then(resolve) {
if (!didResolve) {
listeners.push(resolve);
} else {
resolve();
}
},
};
function resolveThenable() {
didResolve = true;
listeners.forEach(l => l());
}
function Async() {
if (!didResolve) {
ReactTestRenderer.unstable_yield('Suspend!');
throw thenable;
}
ReactTestRenderer.unstable_yield('Async');
return 'Async';
}
const root = ReactTestRenderer.create(
<Placeholder delayMs={1000} fallback={<Text text="Loading..." />}>
<Async />
<Text text="Sibling" />
</Placeholder>,
{
unstable_isConcurrent: true,
},
);
expect(root).toFlushAndYieldThrough(['Suspend!']);
// The promise resolves before the current render phase has completed
resolveThenable();
expect(ReactTestRenderer).toHaveYielded([]);
// Start over from the root, instead of continuing.
expect(root).toFlushAndYield([
// Async renders again *before* Sibling
'Async',
'Sibling',
]);
expect(root).toMatchRenderedOutput('AsyncSibling');
});
});
@@ -649,6 +649,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
it('resolves successfully even if fallback render is pending', async () => {
ReactNoop.render(
<Placeholder delayMs={1000} fallback={<Text text="Loading..." />}>
<AsyncText text="Async" ms={3000} />
</Placeholder>,
);
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
await advanceTimers(1500);
expect(ReactNoop.expire(1500)).toEqual([]);
// Before we have a chance to flush, the promise resolves.
await advanceTimers(2000);
expect(ReactNoop.clearYields()).toEqual(['Promise resolved [Async]']);
expect(ReactNoop.flush()).toEqual(['Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
});
it('throws a helpful error when an update is suspends without a placeholder', () => {
expect(() => {
ReactNoop.flushSync(() =>