Skip to content

Commit

Permalink
Move hide/unhide logic to Offscreen component
Browse files Browse the repository at this point in the history
The Offscreen component is not a public type, yet, but once it is, it
will share the same hide/unhide logic as Suspense children.
  • Loading branch information
acdlite committed Apr 24, 2020
1 parent b0e18cd commit 709e4e3
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/react-reconciler/src/ReactFiber.new.js
Expand Up @@ -20,6 +20,7 @@ import type {WorkTag} from './ReactWorkTags';
import type {TypeOfMode} from './ReactTypeOfMode';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {OffscreenProps} from './ReactFiberOffscreenComponent';

import invariant from 'shared/invariant';
import {
Expand Down Expand Up @@ -738,7 +739,7 @@ export function createFiberFromSuspenseList(
}

export function createFiberFromOffscreen(
pendingProps: any,
pendingProps: OffscreenProps,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
key: null | string,
Expand Down
91 changes: 81 additions & 10 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Expand Up @@ -19,6 +19,10 @@ import type {
SuspenseListTailMode,
} from './ReactFiberSuspenseComponent.new';
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
import type {
OffscreenProps,
OffscreenState,
} from './ReactFiberOffscreenComponent';

import checkPropTypes from 'shared/checkPropTypes';

Expand Down Expand Up @@ -562,7 +566,20 @@ function updateOffscreenComponent(
workInProgress: Fiber,
renderExpirationTime: ExpirationTimeOpaque,
) {
const nextChildren = workInProgress.pendingProps;
const nextProps: OffscreenProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;

if (current !== null) {
if (nextProps.mode === 'hidden') {
// TODO: Should currently be unreachable because Offscreen is only used as
// an implementation detail of Suspense. Once this is a public API, it
// will need to create an OffscreenState.
} else {
// Clear the offscreen state.
workInProgress.memoizedState = null;
}
}

reconcileChildren(
current,
workInProgress,
Expand Down Expand Up @@ -1854,12 +1871,16 @@ function updateSuspenseComponent(
}

if (showFallback) {
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
const fallbackFragment = mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderExpirationTime,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState = ({baseTime: NoWork}: OffscreenState);
workInProgress.memoizedState = mountSuspenseState(renderExpirationTime);
return fallbackFragment;
} else {
Expand Down Expand Up @@ -1904,14 +1925,19 @@ function updateSuspenseComponent(
} else {
// Suspended but we should no longer be in dehydrated mode.
// Therefore we now have to render the fallback.
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderExpirationTime,
);

const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState = ({
baseTime: NoWork,
}: OffscreenState);
workInProgress.memoizedState = updateSuspenseState(
current.memoizedState,
renderExpirationTime,
Expand All @@ -1924,13 +1950,18 @@ function updateSuspenseComponent(

if (showFallback) {
const nextFallbackChildren = nextProps.fallback;
const nextPrimaryChildren = nextProps.children;
const fallbackChildFragment = updateSuspenseFallbackChildren(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderExpirationTime,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState = ({
baseTime: NoWork,
}: OffscreenState);
primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
current,
workInProgress,
Expand All @@ -1957,13 +1988,18 @@ function updateSuspenseComponent(
if (showFallback) {
// Timed out.
const nextFallbackChildren = nextProps.fallback;
const nextPrimaryChildren = nextProps.children;
const fallbackChildFragment = updateSuspenseFallbackChildren(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderExpirationTime,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState = ({
baseTime: NoWork,
}: OffscreenState);
primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
current,
workInProgress,
Expand Down Expand Up @@ -1996,8 +2032,12 @@ function mountSuspensePrimaryChildren(
renderExpirationTime,
) {
const mode = workInProgress.mode;
const primaryChildProps: OffscreenProps = {
mode: 'visible',
children: primaryChildren,
};
const primaryChildFragment = createFiberFromOffscreen(
primaryChildren,
primaryChildProps,
mode,
renderExpirationTime,
null,
Expand All @@ -2009,20 +2049,26 @@ function mountSuspensePrimaryChildren(

function mountSuspenseFallbackChildren(
workInProgress,
primaryChildren,
fallbackChildren,
renderExpirationTime,
) {
const mode = workInProgress.mode;

const progressedPrimaryFragment: Fiber | null = workInProgress.child;

const primaryChildProps: OffscreenProps = {
mode: 'hidden',
children: primaryChildren,
};

let primaryChildFragment;
let fallbackChildFragment;
if ((mode & BlockingMode) === NoMode && progressedPrimaryFragment !== null) {
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childExpirationTime_opaque = NoWork;
primaryChildFragment.pendingProps = primaryChildProps;

if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// Reset the durations from the first pass so they aren't included in the
Expand Down Expand Up @@ -2053,7 +2099,12 @@ function mountSuspenseFallbackChildren(
null,
);
} else {
primaryChildFragment = createFiberFromOffscreen(null, mode, NoWork, null);
primaryChildFragment = createFiberFromOffscreen(
primaryChildProps,
mode,
NoWork,
null,
);
fallbackChildFragment = createFiberFromFragment(
fallbackChildren,
mode,
Expand All @@ -2069,6 +2120,15 @@ function mountSuspenseFallbackChildren(
return fallbackChildFragment;
}

function createWorkInProgressOffscreenFiber(
current: Fiber,
offscreenProps: OffscreenProps,
) {
// The props argument to `createWorkInProgress` is `any` typed, so we use this
// wrapper function to constrain it.
return createWorkInProgress(current, offscreenProps);
}

function updateSuspensePrimaryChildren(
current,
workInProgress,
Expand All @@ -2079,9 +2139,12 @@ function updateSuspensePrimaryChildren(
const currentFallbackChildFragment: Fiber | null =
currentPrimaryChildFragment.sibling;

const primaryChildFragment = createWorkInProgress(
const primaryChildFragment = createWorkInProgressOffscreenFiber(
currentPrimaryChildFragment,
primaryChildren,
{
mode: 'visible',
children: primaryChildren,
},
);
if ((workInProgress.mode & BlockingMode) === NoMode) {
primaryChildFragment.expirationTime_opaque = renderExpirationTime;
Expand All @@ -2102,6 +2165,7 @@ function updateSuspensePrimaryChildren(
function updateSuspenseFallbackChildren(
current,
workInProgress,
primaryChildren,
fallbackChildren,
renderExpirationTime,
) {
Expand All @@ -2110,13 +2174,19 @@ function updateSuspenseFallbackChildren(
const currentFallbackChildFragment: Fiber | null =
currentPrimaryChildFragment.sibling;

const primaryChildProps: OffscreenProps = {
mode: 'hidden',
children: primaryChildren,
};

let primaryChildFragment;
if ((mode & BlockingMode) === NoMode) {
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
const progressedPrimaryFragment: Fiber = (workInProgress.child: any);
primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childExpirationTime_opaque = NoWork;
primaryChildFragment.pendingProps = primaryChildProps;

if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// Reset the durations from the first pass so they aren't included in the
Expand All @@ -2142,9 +2212,9 @@ function updateSuspenseFallbackChildren(
workInProgress.firstEffect = workInProgress.lastEffect = null;
}
} else {
primaryChildFragment = createWorkInProgress(
primaryChildFragment = createWorkInProgressOffscreenFiber(
currentPrimaryChildFragment,
currentPrimaryChildFragment.pendingProps,
primaryChildProps,
);
}
let fallbackChildFragment;
Expand Down Expand Up @@ -2205,12 +2275,13 @@ function retrySuspenseComponentWithoutHydrating(
function mountSuspenseFallbackAfterRetryWithoutHydrating(
current,
workInProgress,
primaryChildren,
fallbackChildren,
renderExpirationTime,
) {
const mode = workInProgress.mode;
const primaryChildFragment = createFiberFromOffscreen(
null,
primaryChildren,
mode,
NoWork,
null,
Expand Down
50 changes: 31 additions & 19 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Expand Up @@ -23,6 +23,7 @@ import type {UpdateQueue} from './ReactUpdateQueue.new';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
import type {Wakeable} from 'shared/ReactTypes';
import type {ReactPriorityLevel} from './ReactInternalTypes';
import type {OffscreenState} from './ReactFiberOffscreenComponent';

import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
import {
Expand Down Expand Up @@ -55,6 +56,7 @@ import {
FundamentalComponent,
ScopeComponent,
Block,
OffscreenComponent,
} from './ReactWorkTags';
import {
invokeGuardedCallback,
Expand Down Expand Up @@ -805,6 +807,7 @@ function commitLifeCycles(
case IncompleteClassComponent:
case FundamentalComponent:
case ScopeComponent:
case OffscreenComponent:
return;
}
invariant(
Expand Down Expand Up @@ -835,16 +838,12 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
unhideTextInstance(instance, node.memoizedProps);
}
} else if (
node.tag === SuspenseComponent &&
node.memoizedState !== null &&
node.memoizedState.dehydrated === null
node.tag === OffscreenComponent &&
(node.memoizedState: OffscreenState) !== null &&
node !== finishedWork
) {
// Found a nested Suspense component that timed out. Skip over the
// primary child fragment, which should remain hidden.
const fallbackChildFragment: Fiber = (node.child: any).sibling;
fallbackChildFragment.return = node;
node = fallbackChildFragment;
continue;
// Found a nested Offscreen component that is hidden. Don't search
// any deeper. This tree should remain hidden.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
Expand Down Expand Up @@ -1584,6 +1583,9 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
break;
}
case OffscreenComponent: {
return;
}
}

commitContainer(finishedWork);
Expand Down Expand Up @@ -1720,6 +1722,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
break;
}
case OffscreenComponent: {
const newState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = newState !== null;
hideOrUnhideAllChildren(finishedWork, isHidden);
return;
}
}
invariant(
false,
Expand All @@ -1731,18 +1739,22 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
function commitSuspenseComponent(finishedWork: Fiber) {
const newState: SuspenseState | null = finishedWork.memoizedState;

let newDidTimeout;
let primaryChildParent = finishedWork;
if (newState === null) {
newDidTimeout = false;
} else {
newDidTimeout = true;
primaryChildParent = finishedWork.child;
if (newState !== null) {
markCommitTimeOfFallback();
}

if (supportsMutation && primaryChildParent !== null) {
hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
if (supportsMutation) {
// Hide the Offscreen component that contains the primary children. TODO:
// Ideally, this effect would have been scheduled on the Offscreen fiber
// itself. That's how unhiding works: the Offscreen component schedules an
// effect on itself. However, in this case, the component didn't complete,
// so the fiber was never added to the effect list in the normal path. We
// could have appended it to the effect list in the Suspense component's
// second pass, but doing it this way is less complicated. This would be
// simpler if we got rid of the effect list and traversed the tree, like
// we're planning to do.
const primaryChildParent: Fiber = (finishedWork.child: any);
hideOrUnhideAllChildren(primaryChildParent, true);
}
}

if (enableSuspenseCallback && newState !== null) {
Expand Down
15 changes: 14 additions & 1 deletion packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Expand Up @@ -26,6 +26,8 @@ import type {
SuspenseListRenderState,
} from './ReactFiberSuspenseComponent.new';
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
import type {OffscreenState} from './ReactFiberOffscreenComponent';

import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';

import {now} from './SchedulerWithReactIntegration.new';
Expand Down Expand Up @@ -1288,8 +1290,19 @@ function completeWork(
return null;
}
break;
case OffscreenComponent:
case OffscreenComponent: {
if (current !== null) {
const nextState: OffscreenState | null = workInProgress.memoizedState;
const prevState: OffscreenState | null = current.memoizedState;

const prevIsHidden = prevState !== null;
const nextIsHidden = nextState !== null;
if (prevIsHidden !== nextIsHidden) {
workInProgress.effectTag |= Update;
}
}
return null;
}
}
invariant(
false,
Expand Down

0 comments on commit 709e4e3

Please sign in to comment.