@samitouri / QOS-React-2 / commits / 788da69b74

[Suspense] Fix bad loading state not being delayed (#15891)

Fixes a bug where a bad loading state is initially suspended, but a subsequent update with the same expiration time causes it to commit immediately.

Andrew Clark committed Jun 14, 2019 at 16:10 UTC 788da69b74d2311ae2669f7936a9a33ec2469241
2 files changed +53 -1
packages/react-reconciler/src/ReactFiberWorkLoop.js
+6 -1
@@ -323,6 +323,9 @@ export function computeExpirationForFiber(
323
324 // If we're in the middle of rendering a tree, do not update at the same
325 // expiration time that is already rendering.
326 + // TODO: We shouldn't have to do this if the update is on a different root.
327 + // Refactor computeExpirationForFiber + scheduleUpdate so we have access to
328 + // the root when we check for this condition.
329 if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
330 // This is a trick to move this update into a separate batch
331 expirationTime -= 1;
@@ -806,8 +809,10 @@ function renderRoot(
809 return null;
810 }
811
809 - if (root.finishedExpirationTime === expirationTime) {
812 + if (isSync && root.finishedExpirationTime === expirationTime) {
813 // There's already a pending commit at this expiration time.
814 + // TODO: This is poorly factored. This case only exists for the
815 + // batch.commit() API.
816 return commitRoot.bind(null, root);
817 }
818
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+47
@@ -2294,4 +2294,51 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2294 expect(ReactNoop.getChildren()).toEqual([span('D')]);
2295 });
2296 });
2297 +
2298 + it("suspended commit remains suspended even if there's another update at same expiration", async () => {
2299 + // Regression test
2300 + function App({text}) {
2301 + return (
2302 + <Suspense fallback="Outer fallback">
2303 + <AsyncText ms={2000} text={text} />
2304 + </Suspense>
2305 + );
2306 + }
2307 +
2308 + const root = ReactNoop.createRoot();
2309 + await ReactNoop.act(async () => {
2310 + root.render(<App text="Initial" />);
2311 + });
2312 +
2313 + // Resolve initial render
2314 + await ReactNoop.act(async () => {
2315 + Scheduler.advanceTime(2000);
2316 + await advanceTimers(2000);
2317 + });
2318 + expect(Scheduler).toHaveYielded([
2319 + 'Suspend! [Initial]',
2320 + 'Promise resolved [Initial]',
2321 + 'Initial',
2322 + ]);
2323 + expect(root).toMatchRenderedOutput(<span prop="Initial" />);
2324 +
2325 + // Suspend B. Since showing a fallback would hide content that's already
2326 + // visible, it should suspend for a bit without committing.
2327 + await ReactNoop.act(async () => {
2328 + root.render(<App text="First update" />);
2329 +
2330 + expect(Scheduler).toFlushAndYield(['Suspend! [First update]']);
2331 + // Should not display a fallback
2332 + expect(root).toMatchRenderedOutput(<span prop="Initial" />);
2333 + });
2334 +
2335 + // Suspend A. This should also suspend for a JND.
2336 + await ReactNoop.act(async () => {
2337 + root.render(<App text="Second update" />);
2338 +
2339 + expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']);
2340 + // Should not display a fallback
2341 + expect(root).toMatchRenderedOutput(<span prop="Initial" />);
2342 + });
2343 + });
2344 });