@samitouri / QOS-React / commits / daee08562c

[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.

Ricky committed Mar 21, 2025 at 14:44 UTC daee08562ccf5abf7108b63f274f5ca669ee7dd5
7 files changed +8 -789
packages/react-reconciler/src/ReactFiber.js
-12
@@ -110,10 +110,6 @@ import {
110 REACT_ACTIVITY_TYPE,
111 } from 'shared/ReactSymbols';
112 import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
113 -import {
114 - detachOffscreenInstance,
115 - attachOffscreenInstance,
116 -} from './ReactFiberCommitWork';
113 import {getHostContext} from './ReactFiberHostContext';
114 import type {ReactComponentInfo} from '../../shared/ReactTypes';
115 import isArray from 'shared/isArray';
@@ -854,13 +850,9 @@ export function createFiberFromOffscreen(
850 fiber.lanes = lanes;
851 const primaryChildInstance: OffscreenInstance = {
852 _visibility: OffscreenVisible,
857 - _pendingVisibility: OffscreenVisible,
853 _pendingMarkers: null,
854 _retryCache: null,
855 _transitions: null,
861 - _current: null,
862 - detach: () => detachOffscreenInstance(primaryChildInstance),
863 - attach: () => attachOffscreenInstance(primaryChildInstance),
856 };
857 fiber.stateNode = primaryChildInstance;
858 return fiber;
@@ -909,13 +901,9 @@ export function createFiberFromLegacyHidden(
901 // the offscreen implementation, which depends on a state node
902 const instance: OffscreenInstance = {
903 _visibility: OffscreenVisible,
912 - _pendingVisibility: OffscreenVisible,
904 _pendingMarkers: null,
905 _transitions: null,
906 _retryCache: null,
916 - _current: null,
917 - detach: () => detachOffscreenInstance(instance),
918 - attach: () => attachOffscreenInstance(instance),
907 };
908 fiber.stateNode = instance;
909 return fiber;
packages/react-reconciler/src/ReactFiberActivityComponent.js
+1 -16
@@ -10,7 +10,6 @@
10 import type {ReactNodeList, OffscreenMode, Wakeable} from 'shared/ReactTypes';
11 import type {Lanes} from './ReactFiberLane';
12 import type {SpawnedCachePool} from './ReactFiberCacheComponent';
13 -import type {Fiber} from './ReactInternalTypes';
13 import type {
14 Transition,
15 TracingMarkerInstance,
@@ -47,25 +46,11 @@ export type OffscreenQueue = {
46 type OffscreenVisibility = number;
47
48 export const OffscreenVisible = /* */ 0b001;
50 -export const OffscreenDetached = /* */ 0b010;
51 -export const OffscreenPassiveEffectsConnected = /* */ 0b100;
49 +export const OffscreenPassiveEffectsConnected = /* */ 0b010;
50
51 export type OffscreenInstance = {
54 - _pendingVisibility: OffscreenVisibility,
52 _visibility: OffscreenVisibility,
53 _pendingMarkers: Set<TracingMarkerInstance> | null,
54 _transitions: Set<Transition> | null,
55 _retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
59 -
60 - // Represents the current Offscreen fiber
61 - _current: Fiber | null,
62 - detach: () => void,
63 - attach: () => void,
56 };
65 -
66 -export function isOffscreenManual(offscreenFiber: Fiber): boolean {
67 - return (
68 - offscreenFiber.memoizedProps !== null &&
69 - offscreenFiber.memoizedProps.mode === 'manual'
70 - );
71 -}
packages/react-reconciler/src/ReactFiberBeginWork.js
+1 -8
@@ -33,7 +33,6 @@ import type {
33 ViewTransitionState,
34 } from './ReactFiberViewTransitionComponent';
35 import {assignViewTransitionAutoName} from './ReactFiberViewTransitionComponent';
36 -import {OffscreenDetached} from './ReactFiberActivityComponent';
36 import type {
37 Cache,
38 CacheComponentState,
@@ -647,19 +646,13 @@ function updateOffscreenComponent(
646 ) {
647 const nextProps: OffscreenProps = workInProgress.pendingProps;
648 const nextChildren = nextProps.children;
650 - const nextIsDetached =
651 - (workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0;
649
650 const prevState: OffscreenState | null =
651 current !== null ? current.memoizedState : null;
652
656 - markRef(current, workInProgress);
657 -
653 if (
654 nextProps.mode === 'hidden' ||
660 - (enableLegacyHidden &&
661 - nextProps.mode === 'unstable-defer-without-hiding') ||
662 - nextIsDetached
655 + (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
656 ) {
657 // Rendering a hidden tree.
658
packages/react-reconciler/src/ReactFiberCommitWork.js
+4 -84
@@ -18,20 +18,15 @@ import type {
18 } from './ReactFiberConfig';
19 import type {Fiber, FiberRoot} from './ReactInternalTypes';
20 import type {Lanes} from './ReactFiberLane';
21 -import {
22 - includesOnlyViewTransitionEligibleLanes,
23 - SyncLane,
24 -} from './ReactFiberLane';
21 +import {includesOnlyViewTransitionEligibleLanes} from './ReactFiberLane';
22 import type {SuspenseState, RetryQueue} from './ReactFiberSuspenseComponent';
23 import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
24 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
25 import type {Wakeable} from 'shared/ReactTypes';
29 -import {isOffscreenManual} from './ReactFiberActivityComponent';
26 import type {
27 OffscreenState,
28 OffscreenInstance,
29 OffscreenQueue,
34 - OffscreenProps,
30 } from './ReactFiberActivityComponent';
31 import type {Cache} from './ReactFiberCacheComponent';
32 import type {RootState} from './ReactFiberRoot';
@@ -194,15 +189,12 @@ import {releaseCache, retainCache} from './ReactFiberCacheComponent';
189 import {clearTransitionsForLanes} from './ReactFiberLane';
190 import {
191 OffscreenVisible,
197 - OffscreenDetached,
192 OffscreenPassiveEffectsConnected,
193 } from './ReactFiberActivityComponent';
194 import {
195 TransitionRoot,
196 TransitionTracingMarker,
197 } from './ReactFiberTracingMarkerComponent';
204 -import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
205 -import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
198 import {
199 commitHookLayoutEffects,
200 commitHookLayoutUnmountEffects,
@@ -742,14 +734,6 @@ function commitLayoutEffectOnFiber(
734 committedLanes,
735 );
736 }
745 - if (flags & Ref) {
746 - const props: OffscreenProps = finishedWork.memoizedProps;
747 - if (props.mode === 'manual') {
748 - safelyAttachRef(finishedWork, finishedWork.return);
749 - } else {
750 - safelyDetachRef(finishedWork, finishedWork.return);
751 - }
752 - }
737 break;
738 }
739 case ViewTransitionComponent: {
@@ -1538,9 +1522,6 @@ function commitDeletionEffectsOnFiber(
1522 return;
1523 }
1524 case OffscreenComponent: {
1541 - if (!offscreenSubtreeWasHidden) {
1542 - safelyDetachRef(deletedFiber, nearestMountedAncestor);
1543 - }
1525 if (disableLegacyMode || deletedFiber.mode & ConcurrentMode) {
1526 // If this offscreen component is hidden, we already unmounted it. Before
1527 // deleting the children, track that it's already unmounted so that we
@@ -1672,48 +1653,6 @@ function getRetryCache(finishedWork: Fiber) {
1653 }
1654 }
1655
1675 -export function detachOffscreenInstance(instance: OffscreenInstance): void {
1676 - const fiber = instance._current;
1677 - if (fiber === null) {
1678 - throw new Error(
1679 - 'Calling Offscreen.detach before instance handle has been set.',
1680 - );
1681 - }
1682 -
1683 - if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) {
1684 - // The instance is already detached, this is a noop.
1685 - return;
1686 - }
1687 -
1688 - // TODO: There is an opportunity to optimise this by not entering commit phase
1689 - // and unmounting effects directly.
1690 - const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
1691 - if (root !== null) {
1692 - instance._pendingVisibility |= OffscreenDetached;
1693 - scheduleUpdateOnFiber(root, fiber, SyncLane);
1694 - }
1695 -}
1696 -
1697 -export function attachOffscreenInstance(instance: OffscreenInstance): void {
1698 - const fiber = instance._current;
1699 - if (fiber === null) {
1700 - throw new Error(
1701 - 'Calling Offscreen.detach before instance handle has been set.',
1702 - );
1703 - }
1704 -
1705 - if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) {
1706 - // The instance is already attached, this is a noop.
1707 - return;
1708 - }
1709 -
1710 - const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
1711 - if (root !== null) {
1712 - instance._pendingVisibility &= ~OffscreenDetached;
1713 - scheduleUpdateOnFiber(root, fiber, SyncLane);
1714 - }
1715 -}
1716 -
1656 function attachSuspenseRetryListeners(
1657 finishedWork: Fiber,
1658 wakeables: RetryQueue,
@@ -2181,12 +2120,6 @@ function commitMutationEffectsOnFiber(
2120 break;
2121 }
2122 case OffscreenComponent: {
2184 - if (flags & Ref) {
2185 - if (!offscreenSubtreeWasHidden && current !== null) {
2186 - safelyDetachRef(current, current.return);
2187 - }
2188 - }
2189 -
2123 const newState: OffscreenState | null = finishedWork.memoizedState;
2124 const isHidden = newState !== null;
2125 const wasHidden = current !== null && current.memoizedState !== null;
@@ -2208,18 +2141,9 @@ function commitMutationEffectsOnFiber(
2141
2142 commitReconciliationEffects(finishedWork, lanes);
2143
2211 - const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
2212 -
2213 - // TODO: Add explicit effect flag to set _current.
2214 - offscreenInstance._current = finishedWork;
2215 -
2216 - // Offscreen stores pending changes to visibility in `_pendingVisibility`. This is
2217 - // to support batching of `attach` and `detach` calls.
2218 - offscreenInstance._visibility &= ~OffscreenDetached;
2219 - offscreenInstance._visibility |=
2220 - offscreenInstance._pendingVisibility & OffscreenDetached;
2221 -
2144 if (flags & Visibility) {
2145 + const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
2146 +
2147 // Track the current state on the Offscreen instance so we can
2148 // read it during an event
2149 if (isHidden) {
@@ -2250,8 +2174,7 @@ function commitMutationEffectsOnFiber(
2174 }
2175 }
2176
2253 - // Offscreen with manual mode manages visibility manually.
2254 - if (supportsMutation && !isOffscreenManual(finishedWork)) {
2177 + if (supportsMutation) {
2178 // TODO: This needs to run whenever there's an insertion or update
2179 // inside a hidden Offscreen tree.
2180 hideOrUnhideAllChildren(finishedWork, isHidden);
@@ -2667,9 +2590,6 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2590 break;
2591 }
2592 case OffscreenComponent: {
2670 - // TODO (Offscreen) Check: flags & RefStatic
2671 - safelyDetachRef(finishedWork, finishedWork.return);
2672 -
2593 const isHidden = finishedWork.memoizedState !== null;
2594 if (isHidden) {
2595 // Nested Offscreen tree is already hidden. Don't disappear
packages/react-reconciler/src/ReactFiberCompleteWork.js
+1 -4
@@ -28,7 +28,6 @@ import type {
28 OffscreenState,
29 OffscreenQueue,
30 } from './ReactFiberActivityComponent';
31 -import {isOffscreenManual} from './ReactFiberActivityComponent';
31 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
32 import type {Cache} from './ReactFiberCacheComponent';
33 import {
@@ -384,12 +383,10 @@ function appendAllChildrenToContainer(
383 if (child !== null) {
384 child.return = node;
385 }
387 - // If Offscreen is not in manual mode, detached tree is hidden from user space.
388 - const _needsVisibilityToggle = !isOffscreenManual(node);
386 appendAllChildrenToContainer(
387 containerChildSet,
388 node,
392 - /* needsVisibilityToggle */ _needsVisibilityToggle,
389 + /* needsVisibilityToggle */ true,
390 /* isHidden */ true,
391 );
392
packages/react-reconciler/src/__tests__/Activity-test.js
-663
@@ -9,7 +9,6 @@ let useLayoutEffect;
9 let useEffect;
10 let useInsertionEffect;
11 let useMemo;
12 -let useRef;
12 let startTransition;
13 let waitForPaint;
14 let waitFor;
@@ -31,7 +30,6 @@ describe('Activity', () => {
30 useLayoutEffect = React.useLayoutEffect;
31 useEffect = React.useEffect;
32 useMemo = React.useMemo;
34 - useRef = React.useRef;
33 startTransition = React.startTransition;
34
35 const InternalTestUtils = require('internal-test-utils');
@@ -46,30 +44,6 @@ describe('Activity', () => {
44 return <span prop={props.text}>{props.children}</span>;
45 }
46
49 - function LoggedText({text, children}) {
50 - useInsertionEffect(() => {
51 - Scheduler.log(`mount insertion ${text}`);
52 - return () => {
53 - Scheduler.log(`unmount insertion ${text}`);
54 - };
55 - });
56 -
57 - useEffect(() => {
58 - Scheduler.log(`mount ${text}`);
59 - return () => {
60 - Scheduler.log(`unmount ${text}`);
61 - };
62 - });
63 -
64 - useLayoutEffect(() => {
65 - Scheduler.log(`mount layout ${text}`);
66 - return () => {
67 - Scheduler.log(`unmount layout ${text}`);
68 - };
69 - });
70 - return <Text text={text}>{children}</Text>;
71 - }
72 -
47 // @gate enableLegacyHidden
48 it('unstable-defer-without-hiding should never toggle the visibility of its children', async () => {
49 function App({mode}) {
@@ -1506,641 +1480,4 @@ describe('Activity', () => {
1480 assertLog([]);
1481 expect(root).toMatchRenderedOutput(<span prop={2} />);
1482 });
1509 -
1510 - describe('manual interactivity', () => {
1511 - // @gate enableActivity
1512 - it('should attach ref only for mode null', async () => {
1513 - let offscreenRef;
1514 -
1515 - function App({mode}) {
1516 - offscreenRef = useRef(null);
1517 - return (
1518 - <Activity
1519 - mode={mode}
1520 - ref={ref => {
1521 - offscreenRef.current = ref;
1522 - }}>
1523 - <div />
1524 - </Activity>
1525 - );
1526 - }
1527 -
1528 - const root = ReactNoop.createRoot();
1529 -
1530 - await act(() => {
1531 - root.render(<App mode={'manual'} />);
1532 - });
1533 -
1534 - expect(offscreenRef.current).not.toBeNull();
1535 -
1536 - await act(() => {
1537 - root.render(<App mode={'visible'} />);
1538 - });
1539 -
1540 - expect(offscreenRef.current).toBeNull();
1541 -
1542 - await act(() => {
1543 - root.render(<App mode={'hidden'} />);
1544 - });
1545 -
1546 - expect(offscreenRef.current).toBeNull();
1547 -
1548 - await act(() => {
1549 - root.render(<App mode={'manual'} />);
1550 - });
1551 -
1552 - expect(offscreenRef.current).not.toBeNull();
1553 - });
1554 -
1555 - // @gate enableActivity
1556 - it('should lower update priority for detached Activity', async () => {
1557 - let updateChildState;
1558 - let updateHighPriorityComponentState;
1559 - let offscreenRef;
1560 -
1561 - function Child() {
1562 - const [state, _stateUpdate] = useState(0);
1563 - updateChildState = _stateUpdate;
1564 - const text = 'Child ' + state;
1565 - return <Text text={text} />;
1566 - }
1567 -
1568 - function HighPriorityComponent(props) {
1569 - const [state, _stateUpdate] = useState(0);
1570 - updateHighPriorityComponentState = _stateUpdate;
1571 - const text = 'HighPriorityComponent ' + state;
1572 - return (
1573 - <>
1574 - <Text text={text} />
1575 - {props.children}
1576 - </>
1577 - );
1578 - }
1579 -
1580 - function App() {
1581 - offscreenRef = useRef(null);
1582 - return (
1583 - <>
1584 - <HighPriorityComponent>
1585 - <Activity mode={'manual'} ref={offscreenRef}>
1586 - <Child />
1587 - </Activity>
1588 - </HighPriorityComponent>
1589 - </>
1590 - );
1591 - }
1592 -
1593 - const root = ReactNoop.createRoot();
1594 -
1595 - await act(() => {
1596 - root.render(<App />);
1597 - });
1598 -
1599 - assertLog(['HighPriorityComponent 0', 'Child 0']);
1600 - expect(root).toMatchRenderedOutput(
1601 - <>
1602 - <span prop="HighPriorityComponent 0" />
1603 - <span prop="Child 0" />
1604 - </>,
1605 - );
1606 -
1607 - expect(offscreenRef.current).not.toBeNull();
1608 -
1609 - // Activity is attached by default. State updates from offscreen are **not defered**.
1610 - await act(async () => {
1611 - updateChildState(1);
1612 - updateHighPriorityComponentState(1);
1613 - await waitForPaint(['HighPriorityComponent 1', 'Child 1']);
1614 - expect(root).toMatchRenderedOutput(
1615 - <>
1616 - <span prop="HighPriorityComponent 1" />
1617 - <span prop="Child 1" />
1618 - </>,
1619 - );
1620 - });
1621 -
1622 - await act(() => {
1623 - offscreenRef.current.detach();
1624 - });
1625 -
1626 - // Activity is detached. State updates from offscreen are **defered**.
1627 - await act(async () => {
1628 - updateChildState(2);
1629 - updateHighPriorityComponentState(2);
1630 - await waitForPaint(['HighPriorityComponent 2']);
1631 - expect(root).toMatchRenderedOutput(
1632 - <>
1633 - <span prop="HighPriorityComponent 2" />
1634 - <span prop="Child 1" />
1635 - </>,
1636 - );
1637 - });
1638 -
1639 - assertLog(['Child 2']);
1640 - expect(root).toMatchRenderedOutput(
1641 - <>
1642 - <span prop="HighPriorityComponent 2" />
1643 - <span prop="Child 2" />
1644 - </>,
1645 - );
1646 -
1647 - await act(() => {
1648 - offscreenRef.current.attach();
1649 - });
1650 -
1651 - // Activity is attached. State updates from offscreen are **not defered**.
1652 - await act(async () => {
1653 - updateChildState(3);
1654 - updateHighPriorityComponentState(3);
1655 - await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
1656 - expect(root).toMatchRenderedOutput(
1657 - <>
1658 - <span prop="HighPriorityComponent 3" />
1659 - <span prop="Child 3" />
1660 - </>,
1661 - );
1662 - });
1663 - });
1664 -
1665 - // @gate enableActivity
1666 - it('defers detachment if called during commit', async () => {
1667 - let updateChildState;
1668 - let updateHighPriorityComponentState;
1669 - let offscreenRef;
1670 - let nextRenderTriggerDetach = false;
1671 - let nextRenderTriggerAttach = false;
1672 -
1673 - function Child() {
1674 - const [state, _stateUpdate] = useState(0);
1675 - updateChildState = _stateUpdate;
1676 - const text = 'Child ' + state;
1677 - return <Text text={text} />;
1678 - }
1679 -
1680 - function HighPriorityComponent(props) {
1681 - const [state, _stateUpdate] = useState(0);
1682 - updateHighPriorityComponentState = _stateUpdate;
1683 - const text = 'HighPriorityComponent ' + state;
1684 - useLayoutEffect(() => {
1685 - if (nextRenderTriggerDetach) {
1686 - _stateUpdate(state + 1);
1687 - updateChildState(state + 1);
1688 - offscreenRef.current.detach();
1689 - nextRenderTriggerDetach = false;
1690 - }
1691 -
1692 - if (nextRenderTriggerAttach) {
1693 - offscreenRef.current.attach();
1694 - nextRenderTriggerAttach = false;
1695 - }
1696 - });
1697 - return (
1698 - <>
1699 - <Text text={text} />
1700 - {props.children}
1701 - </>
1702 - );
1703 - }
1704 -
1705 - function App() {
1706 - offscreenRef = useRef(null);
1707 - return (
1708 - <>
1709 - <HighPriorityComponent>
1710 - <Activity mode={'manual'} ref={offscreenRef}>
1711 - <Child />
1712 - </Activity>
1713 - </HighPriorityComponent>
1714 - </>
1715 - );
1716 - }
1717 -
1718 - const root = ReactNoop.createRoot();
1719 -
1720 - await act(() => {
1721 - root.render(<App />);
1722 - });
1723 -
1724 - assertLog(['HighPriorityComponent 0', 'Child 0']);
1725 -
1726 - nextRenderTriggerDetach = true;
1727 -
1728 - // Activity is attached and gets detached inside useLayoutEffect.
1729 - // State updates from offscreen are **defered**.
1730 - await act(async () => {
1731 - updateChildState(1);
1732 - updateHighPriorityComponentState(1);
1733 - await waitForPaint([
1734 - 'HighPriorityComponent 1',
1735 - 'Child 1',
1736 - 'HighPriorityComponent 2',
1737 - ]);
1738 - expect(root).toMatchRenderedOutput(
1739 - <>
1740 - <span prop="HighPriorityComponent 2" />
1741 - <span prop="Child 1" />
1742 - </>,
1743 - );
1744 - });
1745 -
1746 - assertLog(['Child 2']);
1747 - expect(root).toMatchRenderedOutput(
1748 - <>
1749 - <span prop="HighPriorityComponent 2" />
1750 - <span prop="Child 2" />
1751 - </>,
1752 - );
1753 -
1754 - nextRenderTriggerAttach = true;
1755 -
1756 - // Activity is detached. State updates from offscreen are **defered**.
1757 - // Activity is attached inside useLayoutEffect;
1758 - await act(async () => {
1759 - updateChildState(3);
1760 - updateHighPriorityComponentState(3);
1761 - await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
1762 - expect(root).toMatchRenderedOutput(
1763 - <>
1764 - <span prop="HighPriorityComponent 3" />
1765 - <span prop="Child 3" />
1766 - </>,
1767 - );
1768 - });
1769 - });
1770 - });
1771 -
1772 - // @gate enableActivity
1773 - it('should detach ref if Activity is unmounted', async () => {
1774 - let offscreenRef;
1775 -
1776 - function App({showOffscreen}) {
1777 - offscreenRef = useRef(null);
1778 - return showOffscreen ? (
1779 - <Activity
1780 - mode={'manual'}
1781 - ref={ref => {
1782 - offscreenRef.current = ref;
1783 - }}>
1784 - <div />
1785 - </Activity>
1786 - ) : null;
1787 - }
1788 -
1789 - const root = ReactNoop.createRoot();
1790 -
1791 - await act(() => {
1792 - root.render(<App showOffscreen={true} />);
1793 - });
1794 -
1795 - expect(offscreenRef.current).not.toBeNull();
1796 -
1797 - await act(() => {
1798 - root.render(<App showOffscreen={false} />);
1799 - });
1800 -
1801 - expect(offscreenRef.current).toBeNull();
1802 -
1803 - await act(() => {
1804 - root.render(<App showOffscreen={true} />);
1805 - });
1806 -
1807 - expect(offscreenRef.current).not.toBeNull();
1808 - });
1809 -
1810 - // @gate enableActivity
1811 - it('should detach ref when parent Activity is hidden', async () => {
1812 - let offscreenRef;
1813 -
1814 - function App({mode}) {
1815 - offscreenRef = useRef(null);
1816 - return (
1817 - <Activity mode={mode}>
1818 - <Activity mode={'manual'} ref={offscreenRef}>
1819 - <div />
1820 - </Activity>
1821 - </Activity>
1822 - );
1823 - }
1824 -
1825 - const root = ReactNoop.createRoot();
1826 -
1827 - await act(() => {
1828 - root.render(<App mode={'hidden'} />);
1829 - });
1830 -
1831 - expect(offscreenRef.current).toBeNull();
1832 -
1833 - await act(() => {
1834 - root.render(<App mode={'visible'} />);
1835 - });
1836 -
1837 - expect(offscreenRef.current).not.toBeNull();
1838 - await act(() => {
1839 - root.render(<App mode={'hidden'} />);
1840 - });
1841 -
1842 - expect(offscreenRef.current).toBeNull();
1843 - });
1844 -
1845 - // @gate enableActivity
1846 - it('should change _current', async () => {
1847 - let offscreenRef;
1848 - const root = ReactNoop.createRoot();
1849 -
1850 - function App({children}) {
1851 - offscreenRef = useRef(null);
1852 - return (
1853 - <Activity mode={'manual'} ref={offscreenRef}>
1854 - {children}
1855 - </Activity>
1856 - );
1857 - }
1858 -
1859 - await act(() => {
1860 - root.render(
1861 - <App>
1862 - <div />
1863 - </App>,
1864 - );
1865 - });
1866 -
1867 - expect(offscreenRef.current).not.toBeNull();
1868 - const firstFiber = offscreenRef.current._current;
1869 -
1870 - await act(() => {
1871 - root.render(
1872 - <App>
1873 - <span />
1874 - </App>,
1875 - );
1876 - });
1877 -
1878 - expect(offscreenRef.current._current === firstFiber).toBeFalsy();
1879 - });
1880 -
1881 - // @gate enableActivity
1882 - it('does not mount tree until attach is called', async () => {
1883 - let offscreenRef;
1884 - let spanRef;
1885 -
1886 - function Child() {
1887 - spanRef = useRef(null);
1888 - useEffect(() => {
1889 - Scheduler.log('Mount Child');
1890 - return () => {
1891 - Scheduler.log('Unmount Child');
1892 - };
1893 - });
1894 - useLayoutEffect(() => {
1895 - Scheduler.log('Mount Layout Child');
1896 - return () => {
1897 - Scheduler.log('Unmount Layout Child');
1898 - };
1899 - });
1900 -
1901 - return <span ref={spanRef}>Child</span>;
1902 - }
1903 -
1904 - function App() {
1905 - return (
1906 - <Activity mode={'manual'} ref={el => (offscreenRef = el)}>
1907 - <Child />
1908 - </Activity>
1909 - );
1910 - }
1911 -
1912 - const root = ReactNoop.createRoot();
1913 -
1914 - await act(() => {
1915 - root.render(<App />);
1916 - });
1917 -
1918 - expect(offscreenRef).not.toBeNull();
1919 - expect(spanRef.current).not.toBeNull();
1920 - assertLog(['Mount Layout Child', 'Mount Child']);
1921 -
1922 - await act(() => {
1923 - offscreenRef.detach();
1924 - });
1925 -
1926 - expect(spanRef.current).toBeNull();
1927 - assertLog(['Unmount Layout Child', 'Unmount Child']);
1928 -
1929 - // Calling attach on already attached Activity.
1930 - await act(() => {
1931 - offscreenRef.detach();
1932 - });
1933 -
1934 - assertLog([]);
1935 -
1936 - await act(() => {
1937 - offscreenRef.attach();
1938 - });
1939 -
1940 - expect(spanRef.current).not.toBeNull();
1941 - assertLog(['Mount Layout Child', 'Mount Child']);
1942 -
1943 - // Calling attach on already attached Activity
1944 - offscreenRef.attach();
1945 -
1946 - assertLog([]);
1947 - });
1948 -
1949 - // @gate enableActivity
1950 - it('handles nested manual offscreens', async () => {
1951 - let outerOffscreen;
1952 - let innerOffscreen;
1953 -
1954 - function App() {
1955 - return (
1956 - <LoggedText text={'outer'}>
1957 - <Activity mode={'manual'} ref={el => (outerOffscreen = el)}>
1958 - <LoggedText text={'middle'}>
1959 - <Activity mode={'manual'} ref={el => (innerOffscreen = el)}>
1960 - <LoggedText text={'inner'} />
1961 - </Activity>
1962 - </LoggedText>
1963 - </Activity>
1964 - </LoggedText>
1965 - );
1966 - }
1967 -
1968 - const root = ReactNoop.createRoot();
1969 -
1970 - await act(() => {
1971 - root.render(<App />);
1972 - });
1973 -
1974 - assertLog([
1975 - 'outer',
1976 - 'middle',
1977 - 'inner',
1978 - 'mount insertion inner',
1979 - 'mount insertion middle',
1980 - 'mount insertion outer',
1981 - 'mount layout inner',
1982 - 'mount layout middle',
1983 - 'mount layout outer',
1984 - 'mount inner',
1985 - 'mount middle',
1986 - 'mount outer',
1987 - ]);
1988 -
1989 - expect(outerOffscreen).not.toBeNull();
1990 - expect(innerOffscreen).not.toBeNull();
1991 -
1992 - await act(() => {
1993 - outerOffscreen.detach();
1994 - });
1995 -
1996 - expect(innerOffscreen).toBeNull();
1997 -
1998 - assertLog([
1999 - 'unmount layout middle',
2000 - 'unmount layout inner',
2001 - 'unmount middle',
2002 - 'unmount inner',
2003 - ]);
2004 -
2005 - await act(() => {
2006 - outerOffscreen.attach();
2007 - });
2008 -
2009 - assertLog([
2010 - 'mount layout inner',
2011 - 'mount layout middle',
2012 - 'mount inner',
2013 - 'mount middle',
2014 - ]);
2015 -
2016 - await act(() => {
2017 - innerOffscreen.detach();
2018 - });
2019 -
2020 - assertLog(['unmount layout inner', 'unmount inner']);
2021 -
2022 - // Calling detach on already detached Activity.
2023 - await act(() => {
2024 - innerOffscreen.detach();
2025 - });
2026 -
2027 - assertLog([]);
2028 -
2029 - await act(() => {
2030 - innerOffscreen.attach();
2031 - });
2032 -
2033 - assertLog(['mount layout inner', 'mount inner']);
2034 -
2035 - await act(() => {
2036 - innerOffscreen.detach();
2037 - outerOffscreen.attach();
2038 - });
2039 -
2040 - assertLog(['unmount layout inner', 'unmount inner']);
2041 -
2042 - await act(() => {
2043 - root.render(null);
2044 - });
2045 -
2046 - assertLog([
2047 - 'unmount insertion outer',
2048 - 'unmount layout outer',
2049 - 'unmount insertion middle',
2050 - 'unmount layout middle',
2051 - ...(gate('enableHiddenSubtreeInsertionEffectCleanup')
2052 - ? ['unmount insertion inner']
2053 - : []),
2054 - 'unmount outer',
2055 - 'unmount middle',
2056 - ]);
2057 - });
2058 -
2059 - // @gate enableActivity
2060 - it('batches multiple attach and detach calls scheduled from an event handler', async () => {
2061 - function Child() {
2062 - useEffect(() => {
2063 - Scheduler.log('attach child');
2064 - return () => {
2065 - Scheduler.log('detach child');
2066 - };
2067 - }, []);
2068 - return 'child';
2069 - }
2070 -
2071 - const offscreen = React.createRef(null);
2072 - function App() {
2073 - return (
2074 - <Activity ref={offscreen} mode="manual">
2075 - <Child />
2076 - </Activity>
2077 - );
2078 - }
2079 -
2080 - const root = ReactNoop.createRoot();
2081 - await act(() => {
2082 - root.render(<App />);
2083 - });
2084 -
2085 - assertLog(['attach child']);
2086 -
2087 - await act(() => {
2088 - const instance = offscreen.current;
2089 - // Detach then immediately attach the instance.
2090 - instance.detach();
2091 - instance.attach();
2092 - });
2093 -
2094 - assertLog([]);
2095 -
2096 - await act(() => {
2097 - const instance = offscreen.current;
2098 - instance.detach();
2099 - });
2100 -
2101 - assertLog(['detach child']);
2102 -
2103 - await act(() => {
2104 - const instance = offscreen.current;
2105 - // Attach then immediately detach.
2106 - instance.attach();
2107 - instance.detach();
2108 - });
2109 -
2110 - assertLog([]);
2111 - });
2112 -
2113 - // @gate enableActivity
2114 - it('batches multiple attach and detach calls scheduled from an effect', async () => {
2115 - function Child() {
2116 - useEffect(() => {
2117 - Scheduler.log('attach child');
2118 - return () => {
2119 - Scheduler.log('detach child');
2120 - };
2121 - }, []);
2122 - return 'child';
2123 - }
2124 -
2125 - function App() {
2126 - const offscreen = useRef(null);
2127 - useLayoutEffect(() => {
2128 - const instance = offscreen.current;
2129 - // Detach then immediately attach the instance.
2130 - instance.detach();
2131 - instance.attach();
2132 - }, []);
2133 - return (
2134 - <Activity ref={offscreen} mode="manual">
2135 - <Child />
2136 - </Activity>
2137 - );
2138 - }
2139 -
2140 - const root = ReactNoop.createRoot();
2141 - await act(() => {
2142 - root.render(<App />);
2143 - });
2144 - assertLog(['attach child']);
2145 - });
1483 });
packages/shared/ReactTypes.js
+1 -2
@@ -140,8 +140,7 @@ export type Thenable<T> =
140 export type OffscreenMode =
141 | 'hidden'
142 | 'unstable-defer-without-hiding'
143 - | 'visible'
144 - | 'manual';
143 + | 'visible';
144
145 export type StartTransitionOptions = {
146 name?: string,