Skip to content

Commit b330a29

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 895fe33 commit b330a29

File tree

1 file changed

+77
-65
lines changed

1 file changed

+77
-65
lines changed

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

Lines changed: 77 additions & 65 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(

0 commit comments

Comments
 (0)