From d029d8c251894f3baabfc55a49538191c6c8cf36 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 29 Apr 2019 19:28:32 -0700 Subject: [PATCH 1/4] Avoidable suspense boundaries --- .../src/ReactFiberBeginWork.js | 97 +++++++++++++++++-- .../src/ReactFiberCompleteWork.js | 3 + .../src/ReactFiberSuspenseComponent.js | 36 ++++++- .../src/ReactFiberSuspenseContext.js | 79 +++++++++++++++ .../src/ReactFiberUnwindWork.js | 15 +++ ...tSuspenseWithNoopRenderer-test.internal.js | 69 +++++++++++++ 6 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberSuspenseContext.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8659e010a9847..8940795dac739 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -12,6 +12,7 @@ import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; +import type {SuspenseContext} from './ReactFiberSuspenseContext'; import checkPropTypes from 'prop-types/checkPropTypes'; @@ -104,6 +105,17 @@ import { pushHostContextForEventComponent, pushHostContextForEventTarget, } from './ReactFiberHostContext'; +import { + suspenseStackCursor, + pushSuspenseContext, + popSuspenseContext, + DefaultShallowSuspenseContext, + InvisibleParentSuspenseContext, + ForceSuspenseFallback, + hasSuspenseContext, + setShallowSuspenseContext, + addSubtreeSuspenseContext, +} from './ReactFiberSuspenseContext'; import { pushProvider, propagateContextChange, @@ -1394,32 +1406,65 @@ function updateSuspenseComponent( const mode = workInProgress.mode; const nextProps = workInProgress.pendingProps; + // This is used by DevTools to force a boundary to suspend. if (__DEV__) { if (shouldSuspend(workInProgress)) { workInProgress.effectTag |= DidCapture; } } - // We should attempt to render the primary children unless this boundary - // already suspended during this render (`alreadyCaptured` is true). - let nextState: SuspenseState | null = workInProgress.memoizedState; + let suspenseContext: SuspenseContext = suspenseStackCursor.current; - let nextDidTimeout; - if ((workInProgress.effectTag & DidCapture) === NoEffect) { - // This is the first attempt. - nextState = null; - nextDidTimeout = false; - } else { + let nextState = null; + let nextDidTimeout = false; + + if ( + (workInProgress.effectTag & DidCapture) !== NoEffect || + hasSuspenseContext( + suspenseContext, + (ForceSuspenseFallback: SuspenseContext), + ) + ) { + // This either already captured or is a new mount that was forced into its fallback + // state by a parernt. + const attemptedState: SuspenseState | null = workInProgress.memoizedState; // Something in this boundary's subtree already suspended. Switch to // rendering the fallback children. nextState = { fallbackExpirationTime: - nextState !== null ? nextState.fallbackExpirationTime : NoWork, + attemptedState !== null + ? attemptedState.fallbackExpirationTime + : NoWork, }; nextDidTimeout = true; workInProgress.effectTag &= ~DidCapture; + } else { + // Attempting the main content the main content + if (current === null || current.memoizedState !== null) { + // This is a new mount or this boundary is already showing a fallback state. + // Mark this subtree context as having at least one invisible parent that could + // handle the fallback state. + // Boundaries without fallbacks or should be avoided are not considered since + // they cannot handle preferred fallback states. + if ( + nextProps.fallback !== undefined && + nextProps.unstable_avoidThisFallback !== true + ) { + suspenseContext = addSubtreeSuspenseContext( + suspenseContext, + InvisibleParentSuspenseContext, + ); + } + } } + suspenseContext = setShallowSuspenseContext( + suspenseContext, + DefaultShallowSuspenseContext, + ); + + pushSuspenseContext(workInProgress, suspenseContext); + if (__DEV__) { if ('maxDuration' in nextProps) { if (!didWarnAboutMaxDuration) { @@ -1472,6 +1517,7 @@ function updateSuspenseComponent( tryToClaimNextHydratableInstance(workInProgress); // This could've changed the tag if this was a dehydrated suspense component. if (workInProgress.tag === DehydratedSuspenseComponent) { + popSuspenseContext(workInProgress); return updateDehydratedSuspenseComponent( null, workInProgress, @@ -1713,6 +1759,8 @@ function retrySuspenseComponentWithoutHydrating( current.nextEffect = null; current.effectTag = Deletion; + popSuspenseContext(workInProgress); + // Upgrade this work in progress to a real Suspense component. workInProgress.tag = SuspenseComponent; workInProgress.stateNode = null; @@ -1728,6 +1776,13 @@ function updateDehydratedSuspenseComponent( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) { + pushSuspenseContext( + workInProgress, + setShallowSuspenseContext( + suspenseStackCursor.current, + DefaultShallowSuspenseContext, + ), + ); const suspenseInstance = (workInProgress.stateNode: SuspenseInstance); if (current === null) { // During the first pass, we'll bail out and not drill into the children. @@ -2131,6 +2186,13 @@ function beginWork( renderExpirationTime, ); } else { + pushSuspenseContext( + workInProgress, + setShallowSuspenseContext( + suspenseStackCursor.current, + DefaultShallowSuspenseContext, + ), + ); // The primary children do not have pending work with sufficient // priority. Bailout. const child = bailoutOnAlreadyFinishedWork( @@ -2146,11 +2208,26 @@ function beginWork( return null; } } + } else { + pushSuspenseContext( + workInProgress, + setShallowSuspenseContext( + suspenseStackCursor.current, + DefaultShallowSuspenseContext, + ), + ); } break; } case DehydratedSuspenseComponent: { if (enableSuspenseServerRenderer) { + pushSuspenseContext( + workInProgress, + setShallowSuspenseContext( + suspenseStackCursor.current, + DefaultShallowSuspenseContext, + ), + ); // We know that this component will suspend again because if it has // been unsuspended it has committed as a regular Suspense component. // If it needs to be retried, it should have work scheduled on it. diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index eb10a0f14c045..3d04296ba8460 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -77,6 +77,7 @@ import { getHostContext, popHostContainer, } from './ReactFiberHostContext'; +import {popSuspenseContext} from './ReactFiberSuspenseContext'; import { isContextProvider as isLegacyContextProvider, popContext as popLegacyContext, @@ -667,6 +668,7 @@ function completeWork( case ForwardRef: break; case SuspenseComponent: { + popSuspenseContext(workInProgress); const nextState: null | SuspenseState = workInProgress.memoizedState; if ((workInProgress.effectTag & DidCapture) !== NoEffect) { // Something suspended. Re-render with the fallback children. @@ -777,6 +779,7 @@ function completeWork( } case DehydratedSuspenseComponent: { if (enableSuspenseServerRenderer) { + popSuspenseContext(workInProgress); if (current === null) { let wasHydrated = popHydrationState(workInProgress); invariant( diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index c3d9cfecb7950..aa84023d3119f 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -10,17 +10,43 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {SuspenseContext} from './ReactFiberSuspenseContext'; +import { + suspenseStackCursor, + InvisibleParentSuspenseContext, + hasSuspenseContext, +} from './ReactFiberSuspenseContext'; + export type SuspenseState = {| fallbackExpirationTime: ExpirationTime, |}; export function shouldCaptureSuspense(workInProgress: Fiber): boolean { - // In order to capture, the Suspense component must have a fallback prop. - if (workInProgress.memoizedProps.fallback === undefined) { - return false; - } // If it was the primary children that just suspended, capture and render the // fallback. Otherwise, don't capture and bubble to the next boundary. const nextState: SuspenseState | null = workInProgress.memoizedState; - return nextState === null; + if (nextState !== null) { + return false; + } + const props = workInProgress.memoizedProps; + // In order to capture, the Suspense component must have a fallback prop. + if (props.fallback === undefined) { + return false; + } + // Regular boundaries always capture. + if (props.unstable_avoidThisFallback !== true) { + return true; + } + // If it's a boundary we should avoid, then we prefer to bubble up to the + // parent boundary if it is currently invisible. + if ( + hasSuspenseContext( + suspenseStackCursor.current, + (InvisibleParentSuspenseContext: SuspenseContext), + ) + ) { + return false; + } + // If the parent is not able to handle it, we must handle it. + return true; } diff --git a/packages/react-reconciler/src/ReactFiberSuspenseContext.js b/packages/react-reconciler/src/ReactFiberSuspenseContext.js new file mode 100644 index 0000000000000..71d6009896673 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberSuspenseContext.js @@ -0,0 +1,79 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Fiber} from './ReactFiber'; +import type {StackCursor} from './ReactFiberStack'; + +import {createCursor, push, pop} from './ReactFiberStack'; + +export opaque type SuspenseContext = number; +export opaque type SubtreeSuspenseContext: SuspenseContext = number; +export opaque type ShallowSuspenseContext: SuspenseContext = number; + +const DefaultSuspenseContext: SuspenseContext = 0b00; + +// The Suspense Context is split into two parts. The lower bits is +// inherited deeply down the subtree. The upper bits only affect +// this immediate suspense boundary and gets reset each new +// boundary or suspense list. +const SubtreeSuspenseContextMask: SuspenseContext = 0b01; + +// Subtree Flags: + +// InvisibleParentSuspenseContext indicates that one of our parent Suspense +// boundaries is not currently showing visible main content. +// Either because it is already showing a fallback or is not mounted at all. +// We can use this to determine if it is desirable to trigger a fallback at +// the parent. If not, then we might need to trigger undesirable boundaries +// and/or suspend the commit to avoid hiding the parent content. +export const InvisibleParentSuspenseContext: SubtreeSuspenseContext = 0b01; + +// Shallow Flags: + +export const DefaultShallowSuspenseContext: ShallowSuspenseContext = 0b00; + +// ForceSuspenseFallback can be used by SuspenseList to force newly added +// items into their fallback state during one of the render passes. +export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10; + +export const suspenseStackCursor: StackCursor = createCursor( + DefaultSuspenseContext, +); + +export function hasSuspenseContext( + parentContext: SuspenseContext, + flag: SuspenseContext, +): boolean { + return (parentContext & flag) !== 0; +} + +export function setShallowSuspenseContext( + parentContext: SuspenseContext, + shallowContext: ShallowSuspenseContext, +): SuspenseContext { + return (parentContext & SubtreeSuspenseContextMask) | shallowContext; +} + +export function addSubtreeSuspenseContext( + parentContext: SuspenseContext, + subtreeContext: SubtreeSuspenseContext, +): SuspenseContext { + return parentContext | subtreeContext; +} + +export function pushSuspenseContext( + fiber: Fiber, + newContext: SuspenseContext, +): void { + push(suspenseStackCursor, newContext, fiber); +} + +export function popSuspenseContext(fiber: Fiber): void { + pop(suspenseStackCursor, fiber); +} diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 79525c96ad523..6294221c6ad57 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -55,6 +55,7 @@ import { import {logError} from './ReactFiberCommitWork'; import {getStackByFiberInDevAndProd} from './ReactCurrentFiber'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; +import {popSuspenseContext} from './ReactFiberSuspenseContext'; import { isContextProvider as isLegacyContextProvider, popContext as popLegacyContext, @@ -206,6 +207,9 @@ function throwException( checkForWrongSuspensePriorityInDEV(sourceFiber); + // TODO: If we're not in an invisible subtree, then we need to mark this render + // as needing to suspend for longer to avoid showing this fallback state. + // Schedule the nearest Suspense to re-render the timed out view. let workInProgress = returnFiber; do { @@ -408,6 +412,7 @@ function unwindWork( return null; } case SuspenseComponent: { + popSuspenseContext(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; @@ -419,6 +424,7 @@ function unwindWork( case DehydratedSuspenseComponent: { if (enableSuspenseServerRenderer) { // TODO: popHydrationState + popSuspenseContext(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; @@ -466,6 +472,15 @@ function unwindInterruptedWork(interruptedWork: Fiber) { case HostPortal: popHostContainer(interruptedWork); break; + case SuspenseComponent: + popSuspenseContext(interruptedWork); + break; + case DehydratedSuspenseComponent: + if (enableSuspenseServerRenderer) { + // TODO: popHydrationState + popSuspenseContext(interruptedWork); + } + break; case ContextProvider: popProvider(interruptedWork); break; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 34a72083e87c6..5e0e9cac071da 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -56,6 +56,10 @@ describe('ReactSuspenseWithNoopRenderer', () => { return {type: 'span', children: [], prop, hidden: false}; } + function hiddenSpan(prop) { + return {type: 'span', children: [], prop, hidden: true}; + } + function advanceTimers(ms) { // Note: This advances Jest's virtual time but not React's. Use // ReactNoop.expire for that. @@ -1772,4 +1776,69 @@ describe('ReactSuspenseWithNoopRenderer', () => { {withoutStack: true}, ); }); + + it('shows the parent boundary if the inner boundary should be avoided', async () => { + function Foo({showC}) { + Scheduler.yieldValue('Foo'); + return ( + }> + }> + + {showC ? : null} + + + + ); + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Foo', + 'Suspend! [A]', + 'B', + 'Initial load...', + ]); + // We're still suspended. + expect(ReactNoop.getChildren()).toEqual([]); + // Flush to skip suspended time. + Scheduler.advanceTime(600); + await advanceTimers(600); + expect(ReactNoop.getChildren()).toEqual([span('Initial load...')]); + + // Eventually we resolve and show the data. + Scheduler.advanceTime(5000); + await advanceTimers(5000); + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushAndYield(['A', 'B']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + + // Update to show C + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Foo', + 'A', + 'Suspend! [C]', + 'Updating...', + 'B', + ]); + // Flush to skip suspended time. + Scheduler.advanceTime(600); + await advanceTimers(600); + // Since the optional suspense boundary is already showing its content, + // we have to use the inner fallback instead. + expect(ReactNoop.getChildren()).toEqual([ + hiddenSpan('A'), + span('Updating...'), + span('B'), + ]); + + // Later we load the data. + Scheduler.advanceTime(5000); + await advanceTimers(5000); + expect(Scheduler).toHaveYielded(['Promise resolved [C]']); + expect(Scheduler).toFlushAndYield(['A', 'C']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('C'), span('B')]); + }); }); From edc20daf61c278011efcf3e2496f217de11ba39b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 6 May 2019 23:08:30 -0700 Subject: [PATCH 2/4] Move the context out of SuspenseComponent --- .../src/ReactFiberSuspenseComponent.js | 19 ++++----------- .../src/ReactFiberUnwindWork.js | 23 +++++++++++++++---- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index aa84023d3119f..40db21ebdba0d 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -10,18 +10,14 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {SuspenseContext} from './ReactFiberSuspenseContext'; -import { - suspenseStackCursor, - InvisibleParentSuspenseContext, - hasSuspenseContext, -} from './ReactFiberSuspenseContext'; - export type SuspenseState = {| fallbackExpirationTime: ExpirationTime, |}; -export function shouldCaptureSuspense(workInProgress: Fiber): boolean { +export function shouldCaptureSuspense( + workInProgress: Fiber, + hasInvisibleParent: boolean, +): boolean { // If it was the primary children that just suspended, capture and render the // fallback. Otherwise, don't capture and bubble to the next boundary. const nextState: SuspenseState | null = workInProgress.memoizedState; @@ -39,12 +35,7 @@ export function shouldCaptureSuspense(workInProgress: Fiber): boolean { } // If it's a boundary we should avoid, then we prefer to bubble up to the // parent boundary if it is currently invisible. - if ( - hasSuspenseContext( - suspenseStackCursor.current, - (InvisibleParentSuspenseContext: SuspenseContext), - ) - ) { + if (hasInvisibleParent) { return false; } // If the parent is not able to handle it, we must handle it. diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 6294221c6ad57..c0c67c2536e1b 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -13,6 +13,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue} from './ReactCapturedValue'; import type {Update} from './ReactUpdateQueue'; import type {Thenable} from './ReactFiberScheduler'; +import type {SuspenseContext} from './ReactFiberSuspenseContext'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import getComponentName from 'shared/getComponentName'; @@ -55,7 +56,12 @@ import { import {logError} from './ReactFiberCommitWork'; import {getStackByFiberInDevAndProd} from './ReactCurrentFiber'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; -import {popSuspenseContext} from './ReactFiberSuspenseContext'; +import { + suspenseStackCursor, + InvisibleParentSuspenseContext, + hasSuspenseContext, + popSuspenseContext, +} from './ReactFiberSuspenseContext'; import { isContextProvider as isLegacyContextProvider, popContext as popLegacyContext, @@ -207,15 +213,17 @@ function throwException( checkForWrongSuspensePriorityInDEV(sourceFiber); - // TODO: If we're not in an invisible subtree, then we need to mark this render - // as needing to suspend for longer to avoid showing this fallback state. + let hasInvisibleParentBoundary = hasSuspenseContext( + suspenseStackCursor.current, + (InvisibleParentSuspenseContext: SuspenseContext), + ); // Schedule the nearest Suspense to re-render the timed out view. let workInProgress = returnFiber; do { if ( workInProgress.tag === SuspenseComponent && - shouldCaptureSuspense(workInProgress) + shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary) ) { // Found the nearest boundary. @@ -278,6 +286,13 @@ function throwException( workInProgress.effectTag |= ShouldCapture; workInProgress.expirationTime = renderExpirationTime; + + if (!hasInvisibleParentBoundary) { + // TODO: If we're not in an invisible subtree, then we need to mark this render + // pass as needing to suspend for longer to avoid showing this fallback state. + // We could do it here or when we render the fallback. + } + return; } else if ( enableSuspenseServerRenderer && From 98c9e9cf55e69f263c7972284c75a06d12df364b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 6 May 2019 23:25:58 -0700 Subject: [PATCH 3/4] Use setDefaultShallowSuspenseContext instead of passing 0 GCC doesn't optimize this case. Saves us a couple of bytes and is shorter. --- .../src/ReactFiberBeginWork.js | 28 ++++--------------- .../src/ReactFiberSuspenseContext.js | 8 ++++-- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8940795dac739..aa8290bfdcb10 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -109,11 +109,10 @@ import { suspenseStackCursor, pushSuspenseContext, popSuspenseContext, - DefaultShallowSuspenseContext, InvisibleParentSuspenseContext, ForceSuspenseFallback, hasSuspenseContext, - setShallowSuspenseContext, + setDefaultShallowSuspenseContext, addSubtreeSuspenseContext, } from './ReactFiberSuspenseContext'; import { @@ -1458,10 +1457,7 @@ function updateSuspenseComponent( } } - suspenseContext = setShallowSuspenseContext( - suspenseContext, - DefaultShallowSuspenseContext, - ); + suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); pushSuspenseContext(workInProgress, suspenseContext); @@ -1778,10 +1774,7 @@ function updateDehydratedSuspenseComponent( ) { pushSuspenseContext( workInProgress, - setShallowSuspenseContext( - suspenseStackCursor.current, - DefaultShallowSuspenseContext, - ), + setDefaultShallowSuspenseContext(suspenseStackCursor.current), ); const suspenseInstance = (workInProgress.stateNode: SuspenseInstance); if (current === null) { @@ -2188,10 +2181,7 @@ function beginWork( } else { pushSuspenseContext( workInProgress, - setShallowSuspenseContext( - suspenseStackCursor.current, - DefaultShallowSuspenseContext, - ), + setDefaultShallowSuspenseContext(suspenseStackCursor.current), ); // The primary children do not have pending work with sufficient // priority. Bailout. @@ -2211,10 +2201,7 @@ function beginWork( } else { pushSuspenseContext( workInProgress, - setShallowSuspenseContext( - suspenseStackCursor.current, - DefaultShallowSuspenseContext, - ), + setDefaultShallowSuspenseContext(suspenseStackCursor.current), ); } break; @@ -2223,10 +2210,7 @@ function beginWork( if (enableSuspenseServerRenderer) { pushSuspenseContext( workInProgress, - setShallowSuspenseContext( - suspenseStackCursor.current, - DefaultShallowSuspenseContext, - ), + setDefaultShallowSuspenseContext(suspenseStackCursor.current), ); // We know that this component will suspend again because if it has // been unsuspended it has committed as a regular Suspense component. diff --git a/packages/react-reconciler/src/ReactFiberSuspenseContext.js b/packages/react-reconciler/src/ReactFiberSuspenseContext.js index 71d6009896673..e6a51e7c20a3b 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseContext.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseContext.js @@ -36,8 +36,6 @@ export const InvisibleParentSuspenseContext: SubtreeSuspenseContext = 0b01; // Shallow Flags: -export const DefaultShallowSuspenseContext: ShallowSuspenseContext = 0b00; - // ForceSuspenseFallback can be used by SuspenseList to force newly added // items into their fallback state during one of the render passes. export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10; @@ -53,6 +51,12 @@ export function hasSuspenseContext( return (parentContext & flag) !== 0; } +export function setDefaultShallowSuspenseContext( + parentContext: SuspenseContext, +): SuspenseContext { + return parentContext & SubtreeSuspenseContextMask; +} + export function setShallowSuspenseContext( parentContext: SuspenseContext, shallowContext: ShallowSuspenseContext, From b41583725cafdf19c15d6a434002ea081b19b1f1 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 7 May 2019 16:51:04 -0700 Subject: [PATCH 4/4] Typos --- packages/react-reconciler/src/ReactFiberBeginWork.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index aa8290bfdcb10..1179b947d04cf 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1425,7 +1425,7 @@ function updateSuspenseComponent( ) ) { // This either already captured or is a new mount that was forced into its fallback - // state by a parernt. + // state by a parent. const attemptedState: SuspenseState | null = workInProgress.memoizedState; // Something in this boundary's subtree already suspended. Switch to // rendering the fallback children. @@ -1438,7 +1438,7 @@ function updateSuspenseComponent( nextDidTimeout = true; workInProgress.effectTag &= ~DidCapture; } else { - // Attempting the main content the main content + // Attempting the main content if (current === null || current.memoizedState !== null) { // This is a new mount or this boundary is already showing a fallback state. // Mark this subtree context as having at least one invisible parent that could