Skip to content

Commit 9b12dd6

Browse files
committed
Use LegacyHidden to implement old hidden API
If a host component receives a `hidden` prop, we wrap its children in an Offscreen fiber. This is similar to what we do for Suspense children. The LegacyHidden type happens to share the same implementation as the new Offscreen type, for now, but using separate types allows us to fork the behavior later when we implement our planned changes to the Offscreen API. There are two subtle semantic changes here. One is that the children of the host component will have their visibility toggled using the same mechanism we use for Offscreen and Suspense: find the nearest host node children and give them a style of `display: none`. We didn't used to do this in the old API, because the `hidden` DOM attribute on the parent already hides them. So with this change, we're actually "overhiding" the children. I considered addressing this, but I figure I'll leave it as-is in case we want to expose the LegacyHidden component type temporarily to ease migration of Facebook's internal callers to the Offscreen type. The other subtle semantic change is that, because of the extra fiber that wraps around the children, this pattern will cause the children to lose state: ```js return isHidden ? <div hidden={true} /> : <div />; ``` The reason is that I didn't want to wrap every single host component in an extra fiber. So I only wrap them if a `hidden` prop exists. In the above example, that means the children are conditionally wrapped in an extra fiber, so they don't line up during reconciliation, so they get remounted every time `isHidden` changes. The fix is to rewrite to: ```js return <div hidden={isHidden} />; ``` I don't anticipate this will be a problem at Facebook, especially since we're only supposed to use `hidden` via a userspace wrapper component. (And since the bad pattern isn't very React-y, anyway.) Again, the eventual goal is to delete this completely and replace it with Offscreen.
1 parent 4f0fe82 commit 9b12dd6

1 file changed

Lines changed: 92 additions & 86 deletions

File tree

packages/react-reconciler/src/ReactFiberBeginWork.new.js

Lines changed: 92 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ import invariant from 'shared/invariant';
8080
import shallowEqual from 'shared/shallowEqual';
8181
import getComponentName from 'shared/getComponentName';
8282
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
83-
import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols';
83+
import {
84+
REACT_ELEMENT_TYPE,
85+
REACT_LAZY_TYPE,
86+
REACT_LEGACY_HIDDEN_TYPE,
87+
getIteratorFn,
88+
} from 'shared/ReactSymbols';
8489
import {
8590
getCurrentFiberOwnerNameInDevOrNull,
8691
setIsRendering,
@@ -571,69 +576,69 @@ function updateOffscreenComponent(
571576
const nextProps: OffscreenProps = workInProgress.pendingProps;
572577
const nextChildren = nextProps.children;
573578

574-
let subtreeRenderTime = renderExpirationTime;
575-
if (current !== null) {
576-
if (nextProps.mode === 'hidden') {
577-
// TODO: Should currently be unreachable because Offscreen is only used as
578-
// an implementation detail of Suspense. Once this is a public API, it
579-
// will need to create an OffscreenState.
580-
} else {
581-
const prevState: OffscreenState | null = current.memoizedState;
579+
const prevState: OffscreenState | null =
580+
current !== null ? current.memoizedState : null;
581+
582+
if (nextProps.mode === 'hidden') {
583+
if (
584+
!isSameExpirationTime(renderExpirationTime, (Never: ExpirationTimeOpaque))
585+
) {
586+
let nextBaseTime;
582587
if (prevState !== null) {
583-
const baseTime = prevState.baseTime;
584-
subtreeRenderTime = !isSameOrHigherPriority(
585-
baseTime,
588+
const prevBaseTime = prevState.baseTime;
589+
nextBaseTime = !isSameOrHigherPriority(
590+
prevBaseTime,
586591
renderExpirationTime,
587592
)
588-
? baseTime
593+
? prevBaseTime
589594
: renderExpirationTime;
595+
} else {
596+
nextBaseTime = renderExpirationTime;
597+
}
590598

591-
// Since we're not hidden anymore, reset the state
592-
workInProgress.memoizedState = null;
599+
// Schedule this fiber to re-render at offscreen priority. Then bailout.
600+
if (enableSchedulerTracing) {
601+
markSpawnedWork((Never: ExpirationTimeOpaque));
593602
}
603+
workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never;
604+
const nextState: OffscreenState = {
605+
baseTime: nextBaseTime,
606+
};
607+
workInProgress.memoizedState = nextState;
608+
// We're about to bail out, but we need to push this to the stack anyway
609+
// to avoid a push/pop misalignment.
610+
pushRenderExpirationTime(workInProgress, nextBaseTime);
611+
return null;
612+
} else {
613+
// Rendering at offscreen, so we can clear the base time.
614+
const nextState: OffscreenState = {
615+
baseTime: NoWork,
616+
};
617+
workInProgress.memoizedState = nextState;
618+
pushRenderExpirationTime(workInProgress, renderExpirationTime);
594619
}
595-
}
596-
597-
pushRenderExpirationTime(workInProgress, subtreeRenderTime);
598-
reconcileChildren(
599-
current,
600-
workInProgress,
601-
nextChildren,
602-
renderExpirationTime,
603-
);
604-
return workInProgress.child;
605-
}
606-
607-
function updateLegacyHiddenComponent(
608-
current: Fiber | null,
609-
workInProgress: Fiber,
610-
renderExpirationTime: ExpirationTimeOpaque,
611-
) {
612-
const nextProps: OffscreenProps = workInProgress.pendingProps;
613-
const nextChildren = nextProps.children;
620+
} else {
621+
let subtreeRenderTime;
622+
if (prevState !== null) {
623+
const baseTime = prevState.baseTime;
624+
subtreeRenderTime = !isSameOrHigherPriority(
625+
baseTime,
626+
renderExpirationTime,
627+
)
628+
? baseTime
629+
: renderExpirationTime;
614630

615-
let subtreeRenderTime = renderExpirationTime;
616-
if (current !== null) {
617-
if (nextProps.mode === 'hidden') {
618-
throw Error('TODO');
631+
// Since we're not hidden anymore, reset the state
632+
workInProgress.memoizedState = null;
619633
} else {
620-
const prevState: OffscreenState | null = current.memoizedState;
621-
if (prevState !== null) {
622-
const baseTime = prevState.baseTime;
623-
subtreeRenderTime = !isSameOrHigherPriority(
624-
baseTime,
625-
renderExpirationTime,
626-
)
627-
? baseTime
628-
: renderExpirationTime;
629-
630-
// Since we're not hidden anymore, reset the state
631-
workInProgress.memoizedState = null;
632-
}
634+
// We weren't previously hidden, and we still aren't, so there's nothing
635+
// special to do. Need to push to the stack regardless, though, to avoid
636+
// a push/pop misalignment.
637+
subtreeRenderTime = renderExpirationTime;
633638
}
639+
pushRenderExpirationTime(workInProgress, subtreeRenderTime);
634640
}
635641

636-
pushRenderExpirationTime(workInProgress, subtreeRenderTime);
637642
reconcileChildren(
638643
current,
639644
workInProgress,
@@ -643,6 +648,11 @@ function updateLegacyHiddenComponent(
643648
return workInProgress.child;
644649
}
645650

651+
// Note: These happen to have identical begin phases, for now. We shouldn't hold
652+
// ourselves to this constraint, though. If the behavior diverges, we should
653+
// fork the function.
654+
const updateLegacyHiddenComponent = updateOffscreenComponent;
655+
646656
function updateFragment(
647657
current: Fiber | null,
648658
workInProgress: Fiber,
@@ -1192,21 +1202,23 @@ function updateHostComponent(
11921202

11931203
markRef(current, workInProgress);
11941204

1195-
// Check the host config to see if the children are offscreen/hidden.
11961205
if (
1197-
workInProgress.mode & ConcurrentMode &&
1198-
!isSameExpirationTime(
1199-
renderExpirationTime,
1200-
(Never: ExpirationTimeOpaque),
1201-
) &&
1202-
shouldDeprioritizeSubtree(type, nextProps)
1206+
(workInProgress.mode & ConcurrentMode) !== NoMode &&
1207+
nextProps.hasOwnProperty('hidden')
12031208
) {
1204-
if (enableSchedulerTracing) {
1205-
markSpawnedWork((Never: ExpirationTimeOpaque));
1206-
}
1207-
// Schedule this fiber to re-render at offscreen priority. Then bailout.
1208-
workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never;
1209-
return null;
1209+
const wrappedChildren = {
1210+
$$typeof: REACT_ELEMENT_TYPE,
1211+
type: REACT_LEGACY_HIDDEN_TYPE,
1212+
key: null,
1213+
ref: null,
1214+
props: {
1215+
children: nextChildren,
1216+
// Check the host config to see if the children are offscreen/hidden.
1217+
mode: shouldDeprioritizeSubtree(type, nextProps) ? 'hidden' : 'visible',
1218+
},
1219+
_owner: __DEV__ ? {} : null,
1220+
};
1221+
nextChildren = wrappedChildren;
12101222
}
12111223

12121224
reconcileChildren(
@@ -3250,21 +3262,6 @@ function beginWork(
32503262
break;
32513263
case HostComponent:
32523264
pushHostContext(workInProgress);
3253-
if (
3254-
workInProgress.mode & ConcurrentMode &&
3255-
!isSameExpirationTime(
3256-
renderExpirationTime,
3257-
(Never: ExpirationTimeOpaque),
3258-
) &&
3259-
shouldDeprioritizeSubtree(workInProgress.type, newProps)
3260-
) {
3261-
if (enableSchedulerTracing) {
3262-
markSpawnedWork((Never: ExpirationTimeOpaque));
3263-
}
3264-
// Schedule this fiber to re-render at offscreen priority. Then bailout.
3265-
workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never;
3266-
return null;
3267-
}
32683265
break;
32693266
case ClassComponent: {
32703267
const Component = workInProgress.type;
@@ -3419,13 +3416,22 @@ function beginWork(
34193416
return null;
34203417
}
34213418
}
3422-
case OffscreenComponent: {
3423-
pushRenderExpirationTime(workInProgress, renderExpirationTime);
3424-
break;
3425-
}
3419+
case OffscreenComponent:
34263420
case LegacyHiddenComponent: {
3427-
pushRenderExpirationTime(workInProgress, renderExpirationTime);
3428-
break;
3421+
// Need to check if the tree still needs to be deferred. This is
3422+
// almost identical to the logic used in the normal update path,
3423+
// so we'll just enter that. The only difference is we'll bail out
3424+
// at the next level instead of this one, because the child props
3425+
// have not changed. Which is fine.
3426+
// TODO: Probably should refactor `beginWork` to split the bailout
3427+
// path from the normal path. I'm tempted to do a labeled break here
3428+
// but I won't :)
3429+
workInProgress.expirationTime_opaque = NoWork;
3430+
return updateOffscreenComponent(
3431+
current,
3432+
workInProgress,
3433+
renderExpirationTime,
3434+
);
34293435
}
34303436
}
34313437
return bailoutOnAlreadyFinishedWork(

0 commit comments

Comments
 (0)