[activity] remove ref for now (#32645)

Followup from https://github.com/facebook/react/pull/32499

Manual mode is unused and has some bugs such as revealing hidden
boundaries when manually toggling. We also want to change how manual
mode works, and do some refactors to Activity to make it easier to
support. For now we'll remove it, then add it back after the other
changes we have planned.
This commit is contained in:
Ricky 2025-03-21 14:44:02 -04:00 committed by GitHub
parent ab693a926f
commit daee08562c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 8 additions and 789 deletions

View File

@ -110,10 +110,6 @@ import {
REACT_ACTIVITY_TYPE,
} from 'shared/ReactSymbols';
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
import {
detachOffscreenInstance,
attachOffscreenInstance,
} from './ReactFiberCommitWork';
import {getHostContext} from './ReactFiberHostContext';
import type {ReactComponentInfo} from '../../shared/ReactTypes';
import isArray from 'shared/isArray';
@ -854,13 +850,9 @@ export function createFiberFromOffscreen(
fiber.lanes = lanes;
const primaryChildInstance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingVisibility: OffscreenVisible,
_pendingMarkers: null,
_retryCache: null,
_transitions: null,
_current: null,
detach: () => detachOffscreenInstance(primaryChildInstance),
attach: () => attachOffscreenInstance(primaryChildInstance),
};
fiber.stateNode = primaryChildInstance;
return fiber;
@ -909,13 +901,9 @@ export function createFiberFromLegacyHidden(
// the offscreen implementation, which depends on a state node
const instance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingVisibility: OffscreenVisible,
_pendingMarkers: null,
_transitions: null,
_retryCache: null,
_current: null,
detach: () => detachOffscreenInstance(instance),
attach: () => attachOffscreenInstance(instance),
};
fiber.stateNode = instance;
return fiber;

View File

@ -10,7 +10,6 @@
import type {ReactNodeList, OffscreenMode, Wakeable} from 'shared/ReactTypes';
import type {Lanes} from './ReactFiberLane';
import type {SpawnedCachePool} from './ReactFiberCacheComponent';
import type {Fiber} from './ReactInternalTypes';
import type {
Transition,
TracingMarkerInstance,
@ -47,25 +46,11 @@ export type OffscreenQueue = {
type OffscreenVisibility = number;
export const OffscreenVisible = /* */ 0b001;
export const OffscreenDetached = /* */ 0b010;
export const OffscreenPassiveEffectsConnected = /* */ 0b100;
export const OffscreenPassiveEffectsConnected = /* */ 0b010;
export type OffscreenInstance = {
_pendingVisibility: OffscreenVisibility,
_visibility: OffscreenVisibility,
_pendingMarkers: Set<TracingMarkerInstance> | null,
_transitions: Set<Transition> | null,
_retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
// Represents the current Offscreen fiber
_current: Fiber | null,
detach: () => void,
attach: () => void,
};
export function isOffscreenManual(offscreenFiber: Fiber): boolean {
return (
offscreenFiber.memoizedProps !== null &&
offscreenFiber.memoizedProps.mode === 'manual'
);
}

View File

@ -33,7 +33,6 @@ import type {
ViewTransitionState,
} from './ReactFiberViewTransitionComponent';
import {assignViewTransitionAutoName} from './ReactFiberViewTransitionComponent';
import {OffscreenDetached} from './ReactFiberActivityComponent';
import type {
Cache,
CacheComponentState,
@ -647,19 +646,13 @@ function updateOffscreenComponent(
) {
const nextProps: OffscreenProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
const nextIsDetached =
(workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0;
const prevState: OffscreenState | null =
current !== null ? current.memoizedState : null;
markRef(current, workInProgress);
if (
nextProps.mode === 'hidden' ||
(enableLegacyHidden &&
nextProps.mode === 'unstable-defer-without-hiding') ||
nextIsDetached
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
) {
// Rendering a hidden tree.

View File

@ -18,20 +18,15 @@ import type {
} from './ReactFiberConfig';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
import {
includesOnlyViewTransitionEligibleLanes,
SyncLane,
} from './ReactFiberLane';
import {includesOnlyViewTransitionEligibleLanes} from './ReactFiberLane';
import type {SuspenseState, RetryQueue} from './ReactFiberSuspenseComponent';
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
import type {Wakeable} from 'shared/ReactTypes';
import {isOffscreenManual} from './ReactFiberActivityComponent';
import type {
OffscreenState,
OffscreenInstance,
OffscreenQueue,
OffscreenProps,
} from './ReactFiberActivityComponent';
import type {Cache} from './ReactFiberCacheComponent';
import type {RootState} from './ReactFiberRoot';
@ -194,15 +189,12 @@ import {releaseCache, retainCache} from './ReactFiberCacheComponent';
import {clearTransitionsForLanes} from './ReactFiberLane';
import {
OffscreenVisible,
OffscreenDetached,
OffscreenPassiveEffectsConnected,
} from './ReactFiberActivityComponent';
import {
TransitionRoot,
TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent';
import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
import {
commitHookLayoutEffects,
commitHookLayoutUnmountEffects,
@ -742,14 +734,6 @@ function commitLayoutEffectOnFiber(
committedLanes,
);
}
if (flags & Ref) {
const props: OffscreenProps = finishedWork.memoizedProps;
if (props.mode === 'manual') {
safelyAttachRef(finishedWork, finishedWork.return);
} else {
safelyDetachRef(finishedWork, finishedWork.return);
}
}
break;
}
case ViewTransitionComponent: {
@ -1538,9 +1522,6 @@ function commitDeletionEffectsOnFiber(
return;
}
case OffscreenComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
if (disableLegacyMode || deletedFiber.mode & ConcurrentMode) {
// If this offscreen component is hidden, we already unmounted it. Before
// deleting the children, track that it's already unmounted so that we
@ -1672,48 +1653,6 @@ function getRetryCache(finishedWork: Fiber) {
}
}
export function detachOffscreenInstance(instance: OffscreenInstance): void {
const fiber = instance._current;
if (fiber === null) {
throw new Error(
'Calling Offscreen.detach before instance handle has been set.',
);
}
if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) {
// The instance is already detached, this is a noop.
return;
}
// TODO: There is an opportunity to optimise this by not entering commit phase
// and unmounting effects directly.
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
instance._pendingVisibility |= OffscreenDetached;
scheduleUpdateOnFiber(root, fiber, SyncLane);
}
}
export function attachOffscreenInstance(instance: OffscreenInstance): void {
const fiber = instance._current;
if (fiber === null) {
throw new Error(
'Calling Offscreen.detach before instance handle has been set.',
);
}
if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) {
// The instance is already attached, this is a noop.
return;
}
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
instance._pendingVisibility &= ~OffscreenDetached;
scheduleUpdateOnFiber(root, fiber, SyncLane);
}
}
function attachSuspenseRetryListeners(
finishedWork: Fiber,
wakeables: RetryQueue,
@ -2181,12 +2120,6 @@ function commitMutationEffectsOnFiber(
break;
}
case OffscreenComponent: {
if (flags & Ref) {
if (!offscreenSubtreeWasHidden && current !== null) {
safelyDetachRef(current, current.return);
}
}
const newState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = newState !== null;
const wasHidden = current !== null && current.memoizedState !== null;
@ -2208,18 +2141,9 @@ function commitMutationEffectsOnFiber(
commitReconciliationEffects(finishedWork, lanes);
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
// TODO: Add explicit effect flag to set _current.
offscreenInstance._current = finishedWork;
// Offscreen stores pending changes to visibility in `_pendingVisibility`. This is
// to support batching of `attach` and `detach` calls.
offscreenInstance._visibility &= ~OffscreenDetached;
offscreenInstance._visibility |=
offscreenInstance._pendingVisibility & OffscreenDetached;
if (flags & Visibility) {
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
// Track the current state on the Offscreen instance so we can
// read it during an event
if (isHidden) {
@ -2250,8 +2174,7 @@ function commitMutationEffectsOnFiber(
}
}
// Offscreen with manual mode manages visibility manually.
if (supportsMutation && !isOffscreenManual(finishedWork)) {
if (supportsMutation) {
// TODO: This needs to run whenever there's an insertion or update
// inside a hidden Offscreen tree.
hideOrUnhideAllChildren(finishedWork, isHidden);
@ -2667,9 +2590,6 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
break;
}
case OffscreenComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
const isHidden = finishedWork.memoizedState !== null;
if (isHidden) {
// Nested Offscreen tree is already hidden. Don't disappear

View File

@ -28,7 +28,6 @@ import type {
OffscreenState,
OffscreenQueue,
} from './ReactFiberActivityComponent';
import {isOffscreenManual} from './ReactFiberActivityComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
import type {Cache} from './ReactFiberCacheComponent';
import {
@ -384,12 +383,10 @@ function appendAllChildrenToContainer(
if (child !== null) {
child.return = node;
}
// If Offscreen is not in manual mode, detached tree is hidden from user space.
const _needsVisibilityToggle = !isOffscreenManual(node);
appendAllChildrenToContainer(
containerChildSet,
node,
/* needsVisibilityToggle */ _needsVisibilityToggle,
/* needsVisibilityToggle */ true,
/* isHidden */ true,
);

View File

@ -9,7 +9,6 @@ let useLayoutEffect;
let useEffect;
let useInsertionEffect;
let useMemo;
let useRef;
let startTransition;
let waitForPaint;
let waitFor;
@ -31,7 +30,6 @@ describe('Activity', () => {
useLayoutEffect = React.useLayoutEffect;
useEffect = React.useEffect;
useMemo = React.useMemo;
useRef = React.useRef;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
@ -46,30 +44,6 @@ describe('Activity', () => {
return <span prop={props.text}>{props.children}</span>;
}
function LoggedText({text, children}) {
useInsertionEffect(() => {
Scheduler.log(`mount insertion ${text}`);
return () => {
Scheduler.log(`unmount insertion ${text}`);
};
});
useEffect(() => {
Scheduler.log(`mount ${text}`);
return () => {
Scheduler.log(`unmount ${text}`);
};
});
useLayoutEffect(() => {
Scheduler.log(`mount layout ${text}`);
return () => {
Scheduler.log(`unmount layout ${text}`);
};
});
return <Text text={text}>{children}</Text>;
}
// @gate enableLegacyHidden
it('unstable-defer-without-hiding should never toggle the visibility of its children', async () => {
function App({mode}) {
@ -1506,641 +1480,4 @@ describe('Activity', () => {
assertLog([]);
expect(root).toMatchRenderedOutput(<span prop={2} />);
});
describe('manual interactivity', () => {
// @gate enableActivity
it('should attach ref only for mode null', async () => {
let offscreenRef;
function App({mode}) {
offscreenRef = useRef(null);
return (
<Activity
mode={mode}
ref={ref => {
offscreenRef.current = ref;
}}>
<div />
</Activity>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App mode={'manual'} />);
});
expect(offscreenRef.current).not.toBeNull();
await act(() => {
root.render(<App mode={'visible'} />);
});
expect(offscreenRef.current).toBeNull();
await act(() => {
root.render(<App mode={'hidden'} />);
});
expect(offscreenRef.current).toBeNull();
await act(() => {
root.render(<App mode={'manual'} />);
});
expect(offscreenRef.current).not.toBeNull();
});
// @gate enableActivity
it('should lower update priority for detached Activity', async () => {
let updateChildState;
let updateHighPriorityComponentState;
let offscreenRef;
function Child() {
const [state, _stateUpdate] = useState(0);
updateChildState = _stateUpdate;
const text = 'Child ' + state;
return <Text text={text} />;
}
function HighPriorityComponent(props) {
const [state, _stateUpdate] = useState(0);
updateHighPriorityComponentState = _stateUpdate;
const text = 'HighPriorityComponent ' + state;
return (
<>
<Text text={text} />
{props.children}
</>
);
}
function App() {
offscreenRef = useRef(null);
return (
<>
<HighPriorityComponent>
<Activity mode={'manual'} ref={offscreenRef}>
<Child />
</Activity>
</HighPriorityComponent>
</>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog(['HighPriorityComponent 0', 'Child 0']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 0" />
<span prop="Child 0" />
</>,
);
expect(offscreenRef.current).not.toBeNull();
// Activity is attached by default. State updates from offscreen are **not defered**.
await act(async () => {
updateChildState(1);
updateHighPriorityComponentState(1);
await waitForPaint(['HighPriorityComponent 1', 'Child 1']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 1" />
<span prop="Child 1" />
</>,
);
});
await act(() => {
offscreenRef.current.detach();
});
// Activity is detached. State updates from offscreen are **defered**.
await act(async () => {
updateChildState(2);
updateHighPriorityComponentState(2);
await waitForPaint(['HighPriorityComponent 2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 2" />
<span prop="Child 1" />
</>,
);
});
assertLog(['Child 2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 2" />
<span prop="Child 2" />
</>,
);
await act(() => {
offscreenRef.current.attach();
});
// Activity is attached. State updates from offscreen are **not defered**.
await act(async () => {
updateChildState(3);
updateHighPriorityComponentState(3);
await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 3" />
<span prop="Child 3" />
</>,
);
});
});
// @gate enableActivity
it('defers detachment if called during commit', async () => {
let updateChildState;
let updateHighPriorityComponentState;
let offscreenRef;
let nextRenderTriggerDetach = false;
let nextRenderTriggerAttach = false;
function Child() {
const [state, _stateUpdate] = useState(0);
updateChildState = _stateUpdate;
const text = 'Child ' + state;
return <Text text={text} />;
}
function HighPriorityComponent(props) {
const [state, _stateUpdate] = useState(0);
updateHighPriorityComponentState = _stateUpdate;
const text = 'HighPriorityComponent ' + state;
useLayoutEffect(() => {
if (nextRenderTriggerDetach) {
_stateUpdate(state + 1);
updateChildState(state + 1);
offscreenRef.current.detach();
nextRenderTriggerDetach = false;
}
if (nextRenderTriggerAttach) {
offscreenRef.current.attach();
nextRenderTriggerAttach = false;
}
});
return (
<>
<Text text={text} />
{props.children}
</>
);
}
function App() {
offscreenRef = useRef(null);
return (
<>
<HighPriorityComponent>
<Activity mode={'manual'} ref={offscreenRef}>
<Child />
</Activity>
</HighPriorityComponent>
</>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog(['HighPriorityComponent 0', 'Child 0']);
nextRenderTriggerDetach = true;
// Activity is attached and gets detached inside useLayoutEffect.
// State updates from offscreen are **defered**.
await act(async () => {
updateChildState(1);
updateHighPriorityComponentState(1);
await waitForPaint([
'HighPriorityComponent 1',
'Child 1',
'HighPriorityComponent 2',
]);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 2" />
<span prop="Child 1" />
</>,
);
});
assertLog(['Child 2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 2" />
<span prop="Child 2" />
</>,
);
nextRenderTriggerAttach = true;
// Activity is detached. State updates from offscreen are **defered**.
// Activity is attached inside useLayoutEffect;
await act(async () => {
updateChildState(3);
updateHighPriorityComponentState(3);
await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 3" />
<span prop="Child 3" />
</>,
);
});
});
});
// @gate enableActivity
it('should detach ref if Activity is unmounted', async () => {
let offscreenRef;
function App({showOffscreen}) {
offscreenRef = useRef(null);
return showOffscreen ? (
<Activity
mode={'manual'}
ref={ref => {
offscreenRef.current = ref;
}}>
<div />
</Activity>
) : null;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App showOffscreen={true} />);
});
expect(offscreenRef.current).not.toBeNull();
await act(() => {
root.render(<App showOffscreen={false} />);
});
expect(offscreenRef.current).toBeNull();
await act(() => {
root.render(<App showOffscreen={true} />);
});
expect(offscreenRef.current).not.toBeNull();
});
// @gate enableActivity
it('should detach ref when parent Activity is hidden', async () => {
let offscreenRef;
function App({mode}) {
offscreenRef = useRef(null);
return (
<Activity mode={mode}>
<Activity mode={'manual'} ref={offscreenRef}>
<div />
</Activity>
</Activity>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App mode={'hidden'} />);
});
expect(offscreenRef.current).toBeNull();
await act(() => {
root.render(<App mode={'visible'} />);
});
expect(offscreenRef.current).not.toBeNull();
await act(() => {
root.render(<App mode={'hidden'} />);
});
expect(offscreenRef.current).toBeNull();
});
// @gate enableActivity
it('should change _current', async () => {
let offscreenRef;
const root = ReactNoop.createRoot();
function App({children}) {
offscreenRef = useRef(null);
return (
<Activity mode={'manual'} ref={offscreenRef}>
{children}
</Activity>
);
}
await act(() => {
root.render(
<App>
<div />
</App>,
);
});
expect(offscreenRef.current).not.toBeNull();
const firstFiber = offscreenRef.current._current;
await act(() => {
root.render(
<App>
<span />
</App>,
);
});
expect(offscreenRef.current._current === firstFiber).toBeFalsy();
});
// @gate enableActivity
it('does not mount tree until attach is called', async () => {
let offscreenRef;
let spanRef;
function Child() {
spanRef = useRef(null);
useEffect(() => {
Scheduler.log('Mount Child');
return () => {
Scheduler.log('Unmount Child');
};
});
useLayoutEffect(() => {
Scheduler.log('Mount Layout Child');
return () => {
Scheduler.log('Unmount Layout Child');
};
});
return <span ref={spanRef}>Child</span>;
}
function App() {
return (
<Activity mode={'manual'} ref={el => (offscreenRef = el)}>
<Child />
</Activity>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
expect(offscreenRef).not.toBeNull();
expect(spanRef.current).not.toBeNull();
assertLog(['Mount Layout Child', 'Mount Child']);
await act(() => {
offscreenRef.detach();
});
expect(spanRef.current).toBeNull();
assertLog(['Unmount Layout Child', 'Unmount Child']);
// Calling attach on already attached Activity.
await act(() => {
offscreenRef.detach();
});
assertLog([]);
await act(() => {
offscreenRef.attach();
});
expect(spanRef.current).not.toBeNull();
assertLog(['Mount Layout Child', 'Mount Child']);
// Calling attach on already attached Activity
offscreenRef.attach();
assertLog([]);
});
// @gate enableActivity
it('handles nested manual offscreens', async () => {
let outerOffscreen;
let innerOffscreen;
function App() {
return (
<LoggedText text={'outer'}>
<Activity mode={'manual'} ref={el => (outerOffscreen = el)}>
<LoggedText text={'middle'}>
<Activity mode={'manual'} ref={el => (innerOffscreen = el)}>
<LoggedText text={'inner'} />
</Activity>
</LoggedText>
</Activity>
</LoggedText>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog([
'outer',
'middle',
'inner',
'mount insertion inner',
'mount insertion middle',
'mount insertion outer',
'mount layout inner',
'mount layout middle',
'mount layout outer',
'mount inner',
'mount middle',
'mount outer',
]);
expect(outerOffscreen).not.toBeNull();
expect(innerOffscreen).not.toBeNull();
await act(() => {
outerOffscreen.detach();
});
expect(innerOffscreen).toBeNull();
assertLog([
'unmount layout middle',
'unmount layout inner',
'unmount middle',
'unmount inner',
]);
await act(() => {
outerOffscreen.attach();
});
assertLog([
'mount layout inner',
'mount layout middle',
'mount inner',
'mount middle',
]);
await act(() => {
innerOffscreen.detach();
});
assertLog(['unmount layout inner', 'unmount inner']);
// Calling detach on already detached Activity.
await act(() => {
innerOffscreen.detach();
});
assertLog([]);
await act(() => {
innerOffscreen.attach();
});
assertLog(['mount layout inner', 'mount inner']);
await act(() => {
innerOffscreen.detach();
outerOffscreen.attach();
});
assertLog(['unmount layout inner', 'unmount inner']);
await act(() => {
root.render(null);
});
assertLog([
'unmount insertion outer',
'unmount layout outer',
'unmount insertion middle',
'unmount layout middle',
...(gate('enableHiddenSubtreeInsertionEffectCleanup')
? ['unmount insertion inner']
: []),
'unmount outer',
'unmount middle',
]);
});
// @gate enableActivity
it('batches multiple attach and detach calls scheduled from an event handler', async () => {
function Child() {
useEffect(() => {
Scheduler.log('attach child');
return () => {
Scheduler.log('detach child');
};
}, []);
return 'child';
}
const offscreen = React.createRef(null);
function App() {
return (
<Activity ref={offscreen} mode="manual">
<Child />
</Activity>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog(['attach child']);
await act(() => {
const instance = offscreen.current;
// Detach then immediately attach the instance.
instance.detach();
instance.attach();
});
assertLog([]);
await act(() => {
const instance = offscreen.current;
instance.detach();
});
assertLog(['detach child']);
await act(() => {
const instance = offscreen.current;
// Attach then immediately detach.
instance.attach();
instance.detach();
});
assertLog([]);
});
// @gate enableActivity
it('batches multiple attach and detach calls scheduled from an effect', async () => {
function Child() {
useEffect(() => {
Scheduler.log('attach child');
return () => {
Scheduler.log('detach child');
};
}, []);
return 'child';
}
function App() {
const offscreen = useRef(null);
useLayoutEffect(() => {
const instance = offscreen.current;
// Detach then immediately attach the instance.
instance.detach();
instance.attach();
}, []);
return (
<Activity ref={offscreen} mode="manual">
<Child />
</Activity>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog(['attach child']);
});
});

View File

@ -140,8 +140,7 @@ export type Thenable<T> =
export type OffscreenMode =
| 'hidden'
| 'unstable-defer-without-hiding'
| 'visible'
| 'manual';
| 'visible';
export type StartTransitionOptions = {
name?: string,