@samitouri / QOS-React-2 / commits / 14072ce648

Add detach to Offscreen component (#25265)

Samuel Susla committed Oct 18, 2022 at 15:56 UTC 14072ce6489d8968d3687b4410da4c9f59f87cb5
13 files changed +350 -17
packages/react-reconciler/src/ReactFiber.new.js
+5 -1
@@ -73,7 +73,6 @@ import {
73 } from './ReactWorkTags';
74 import {OffscreenVisible} from './ReactFiberOffscreenComponent';
75 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
76 -
76 import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
77 import {
78 resolveClassForHotReloading,
@@ -109,6 +108,7 @@ import {
108 REACT_TRACING_MARKER_TYPE,
109 } from 'shared/ReactSymbols';
110 import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.new';
111 +import {detachOffscreenInstance} from './ReactFiberCommitWork.new';
112
113 export type {Fiber};
114
@@ -755,6 +755,8 @@ export function createFiberFromOffscreen(
755 _pendingMarkers: null,
756 _retryCache: null,
757 _transitions: null,
758 + _current: null,
759 + detach: () => detachOffscreenInstance(primaryChildInstance),
760 };
761 fiber.stateNode = primaryChildInstance;
762 return fiber;
@@ -776,6 +778,8 @@ export function createFiberFromLegacyHidden(
778 _pendingMarkers: null,
779 _transitions: null,
780 _retryCache: null,
781 + _current: null,
782 + detach: () => detachOffscreenInstance(instance),
783 };
784 fiber.stateNode = instance;
785 return fiber;
packages/react-reconciler/src/ReactFiber.old.js
+5 -1
@@ -73,7 +73,6 @@ import {
73 } from './ReactWorkTags';
74 import {OffscreenVisible} from './ReactFiberOffscreenComponent';
75 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
76 -
76 import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
77 import {
78 resolveClassForHotReloading,
@@ -109,6 +108,7 @@ import {
108 REACT_TRACING_MARKER_TYPE,
109 } from 'shared/ReactSymbols';
110 import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.old';
111 +import {detachOffscreenInstance} from './ReactFiberCommitWork.old';
112
113 export type {Fiber};
114
@@ -755,6 +755,8 @@ export function createFiberFromOffscreen(
755 _pendingMarkers: null,
756 _retryCache: null,
757 _transitions: null,
758 + _current: null,
759 + detach: () => detachOffscreenInstance(primaryChildInstance),
760 };
761 fiber.stateNode = primaryChildInstance;
762 return fiber;
@@ -776,6 +778,8 @@ export function createFiberFromLegacyHidden(
778 _pendingMarkers: null,
779 _transitions: null,
780 _retryCache: null,
781 + _current: null,
782 + detach: () => detachOffscreenInstance(instance),
783 };
784 fiber.stateNode = instance;
785 return fiber;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+5 -2
@@ -29,6 +29,7 @@ import type {
29 OffscreenQueue,
30 OffscreenInstance,
31 } from './ReactFiberOffscreenComponent';
32 +import {OffscreenDetached} from './ReactFiberOffscreenComponent';
33 import type {
34 Cache,
35 CacheComponentState,
@@ -37,7 +38,6 @@ import type {
38 import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
39 import type {RootState} from './ReactFiberRoot.new';
40 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
40 -
41 import checkPropTypes from 'shared/checkPropTypes';
42 import {
43 markComponentRenderStarted,
@@ -688,7 +688,10 @@ function updateOffscreenComponent(
688
689 if (
690 nextProps.mode === 'hidden' ||
691 - (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
691 + (enableLegacyHidden &&
692 + nextProps.mode === 'unstable-defer-without-hiding') ||
693 + // TODO: remove read from stateNode.
694 + workInProgress.stateNode._visibility & OffscreenDetached
695 ) {
696 // Rendering a hidden tree.
697
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+5 -2
@@ -29,6 +29,7 @@ import type {
29 OffscreenQueue,
30 OffscreenInstance,
31 } from './ReactFiberOffscreenComponent';
32 +import {OffscreenDetached} from './ReactFiberOffscreenComponent';
33 import type {
34 Cache,
35 CacheComponentState,
@@ -37,7 +38,6 @@ import type {
38 import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
39 import type {RootState} from './ReactFiberRoot.old';
40 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
40 -
41 import checkPropTypes from 'shared/checkPropTypes';
42 import {
43 markComponentRenderStarted,
@@ -688,7 +688,10 @@ function updateOffscreenComponent(
688
689 if (
690 nextProps.mode === 'hidden' ||
691 - (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
691 + (enableLegacyHidden &&
692 + nextProps.mode === 'unstable-defer-without-hiding') ||
693 + // TODO: remove read from stateNode.
694 + workInProgress.stateNode._visibility & OffscreenDetached
695 ) {
696 // Rendering a hidden tree.
697
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+30 -1
@@ -25,6 +25,7 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
25 import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
26 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
27 import type {Wakeable} from 'shared/ReactTypes';
28 +import {isOffscreenManual} from './ReactFiberOffscreenComponent';
29 import type {
30 OffscreenState,
31 OffscreenInstance,
@@ -156,6 +157,7 @@ import {
157 clearSingleton,
158 acquireSingletonInstance,
159 releaseSingletonInstance,
160 + scheduleMicrotask,
161 } from './ReactFiberHostConfig';
162 import {
163 captureCommitPhaseError,
@@ -172,6 +174,7 @@ import {
174 setIsRunningInsertionEffect,
175 getExecutionContext,
176 CommitContext,
177 + RenderContext,
178 NoContext,
179 } from './ReactFiberWorkLoop.new';
180 import {
@@ -200,6 +203,7 @@ import {releaseCache, retainCache} from './ReactFiberCacheComponent.new';
203 import {clearTransitionsForLanes} from './ReactFiberLane.new';
204 import {
205 OffscreenVisible,
206 + OffscreenDetached,
207 OffscreenPassiveEffectsConnected,
208 } from './ReactFiberOffscreenComponent';
209 import {
@@ -2416,6 +2420,28 @@ function getRetryCache(finishedWork) {
2420 }
2421 }
2422
2423 +export function detachOffscreenInstance(instance: OffscreenInstance): void {
2424 + const currentOffscreenFiber = instance._current;
2425 + if (currentOffscreenFiber === null) {
2426 + throw new Error(
2427 + 'Calling Offscreen.detach before instance handle has been set.',
2428 + );
2429 + }
2430 +
2431 + const executionContext = getExecutionContext();
2432 + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
2433 + scheduleMicrotask(() => {
2434 + instance._visibility |= OffscreenDetached;
2435 + disappearLayoutEffects(currentOffscreenFiber);
2436 + disconnectPassiveEffect(currentOffscreenFiber);
2437 + });
2438 + } else {
2439 + instance._visibility |= OffscreenDetached;
2440 + disappearLayoutEffects(currentOffscreenFiber);
2441 + disconnectPassiveEffect(currentOffscreenFiber);
2442 + }
2443 +}
2444 +
2445 function attachSuspenseRetryListeners(
2446 finishedWork: Fiber,
2447 wakeables: Set<Wakeable>,
@@ -2845,6 +2871,8 @@ function commitMutationEffectsOnFiber(
2871 }
2872
2873 commitReconciliationEffects(finishedWork);
2874 + // TODO: Add explicit effect flag to set _current.
2875 + finishedWork.stateNode._current = finishedWork;
2876
2877 if (flags & Visibility) {
2878 const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
@@ -2871,7 +2899,8 @@ function commitMutationEffectsOnFiber(
2899 }
2900 }
2901
2874 - if (supportsMutation) {
2902 + // Offscreen with manual mode manages visibility manually.
2903 + if (supportsMutation && !isOffscreenManual(finishedWork)) {
2904 // TODO: This needs to run whenever there's an insertion or update
2905 // inside a hidden Offscreen tree.
2906 hideOrUnhideAllChildren(offscreenBoundary, isHidden);
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+30 -1
@@ -25,6 +25,7 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
25 import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
26 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
27 import type {Wakeable} from 'shared/ReactTypes';
28 +import {isOffscreenManual} from './ReactFiberOffscreenComponent';
29 import type {
30 OffscreenState,
31 OffscreenInstance,
@@ -156,6 +157,7 @@ import {
157 clearSingleton,
158 acquireSingletonInstance,
159 releaseSingletonInstance,
160 + scheduleMicrotask,
161 } from './ReactFiberHostConfig';
162 import {
163 captureCommitPhaseError,
@@ -172,6 +174,7 @@ import {
174 setIsRunningInsertionEffect,
175 getExecutionContext,
176 CommitContext,
177 + RenderContext,
178 NoContext,
179 } from './ReactFiberWorkLoop.old';
180 import {
@@ -200,6 +203,7 @@ import {releaseCache, retainCache} from './ReactFiberCacheComponent.old';
203 import {clearTransitionsForLanes} from './ReactFiberLane.old';
204 import {
205 OffscreenVisible,
206 + OffscreenDetached,
207 OffscreenPassiveEffectsConnected,
208 } from './ReactFiberOffscreenComponent';
209 import {
@@ -2416,6 +2420,28 @@ function getRetryCache(finishedWork) {
2420 }
2421 }
2422
2423 +export function detachOffscreenInstance(instance: OffscreenInstance): void {
2424 + const currentOffscreenFiber = instance._current;
2425 + if (currentOffscreenFiber === null) {
2426 + throw new Error(
2427 + 'Calling Offscreen.detach before instance handle has been set.',
2428 + );
2429 + }
2430 +
2431 + const executionContext = getExecutionContext();
2432 + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
2433 + scheduleMicrotask(() => {
2434 + instance._visibility |= OffscreenDetached;
2435 + disappearLayoutEffects(currentOffscreenFiber);
2436 + disconnectPassiveEffect(currentOffscreenFiber);
2437 + });
2438 + } else {
2439 + instance._visibility |= OffscreenDetached;
2440 + disappearLayoutEffects(currentOffscreenFiber);
2441 + disconnectPassiveEffect(currentOffscreenFiber);
2442 + }
2443 +}
2444 +
2445 function attachSuspenseRetryListeners(
2446 finishedWork: Fiber,
2447 wakeables: Set<Wakeable>,
@@ -2845,6 +2871,8 @@ function commitMutationEffectsOnFiber(
2871 }
2872
2873 commitReconciliationEffects(finishedWork);
2874 + // TODO: Add explicit effect flag to set _current.
2875 + finishedWork.stateNode._current = finishedWork;
2876
2877 if (flags & Visibility) {
2878 const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
@@ -2871,7 +2899,8 @@ function commitMutationEffectsOnFiber(
2899 }
2900 }
2901
2874 - if (supportsMutation) {
2902 + // Offscreen with manual mode manages visibility manually.
2903 + if (supportsMutation && !isOffscreenManual(finishedWork)) {
2904 // TODO: This needs to run whenever there's an insertion or update
2905 // inside a hidden Offscreen tree.
2906 hideOrUnhideAllChildren(offscreenBoundary, isHidden);
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+9 -1
@@ -26,6 +26,7 @@ import type {
26 SuspenseState,
27 SuspenseListRenderState,
28 } from './ReactFiberSuspenseComponent.new';
29 +import {isOffscreenManual} from './ReactFiberOffscreenComponent';
30 import type {OffscreenState} from './ReactFiberOffscreenComponent';
31 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
32 import type {Cache} from './ReactFiberCacheComponent.new';
@@ -425,7 +426,14 @@ if (supportsMutation) {
426 if (child !== null) {
427 child.return = node;
428 }
428 - appendAllChildrenToContainer(containerChildSet, node, true, true);
429 + // If Offscreen is not in manual mode, detached tree is hidden from user space.
430 + const _needsVisibilityToggle = !isOffscreenManual(node);
431 + appendAllChildrenToContainer(
432 + containerChildSet,
433 + node,
434 + _needsVisibilityToggle,
435 + true,
436 + );
437 } else if (node.child !== null) {
438 node.child.return = node;
439 node = node.child;
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+9 -1
@@ -26,6 +26,7 @@ import type {
26 SuspenseState,
27 SuspenseListRenderState,
28 } from './ReactFiberSuspenseComponent.old';
29 +import {isOffscreenManual} from './ReactFiberOffscreenComponent';
30 import type {OffscreenState} from './ReactFiberOffscreenComponent';
31 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
32 import type {Cache} from './ReactFiberCacheComponent.old';
@@ -425,7 +426,14 @@ if (supportsMutation) {
426 if (child !== null) {
427 child.return = node;
428 }
428 - appendAllChildrenToContainer(containerChildSet, node, true, true);
429 + // If Offscreen is not in manual mode, detached tree is hidden from user space.
430 + const _needsVisibilityToggle = !isOffscreenManual(node);
431 + appendAllChildrenToContainer(
432 + containerChildSet,
433 + node,
434 + _needsVisibilityToggle,
435 + true,
436 + );
437 } else if (node.child !== null) {
438 node.child.return = node;
439 node = node.child;
packages/react-reconciler/src/ReactFiberOffscreenComponent.js
+17 -2
@@ -10,6 +10,7 @@
10 import type {ReactNodeList, OffscreenMode, Wakeable} from 'shared/ReactTypes';
11 import type {Lanes} from './ReactFiberLane.old';
12 import type {SpawnedCachePool} from './ReactFiberCacheComponent.new';
13 +import type {Fiber} from './ReactInternalTypes';
14 import type {
15 Transition,
16 TracingMarkerInstance,
@@ -44,8 +45,9 @@ export type OffscreenQueue = {
45
46 type OffscreenVisibility = number;
47
47 -export const OffscreenVisible = /* */ 0b01;
48 -export const OffscreenPassiveEffectsConnected = /* */ 0b10;
48 +export const OffscreenVisible = /* */ 0b001;
49 +export const OffscreenDetached = /* */ 0b010;
50 +export const OffscreenPassiveEffectsConnected = /* */ 0b100;
51
52 export type OffscreenInstance = {
53 _visibility: OffscreenVisibility,
@@ -53,4 +55,17 @@ export type OffscreenInstance = {
55 _transitions: Set<Transition> | null,
56 // $FlowFixMe[incompatible-type-arg] found when upgrading Flow
57 _retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
58 +
59 + // Represents the current Offscreen fiber
60 + _current: Fiber | null,
61 + detach: () => void,
62 +
63 + // TODO: attach
64 };
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/ReactFiberWorkLoop.new.js
+1 -1
@@ -290,7 +290,7 @@ type ExecutionContext = number;
290
291 export const NoContext = /* */ 0b000;
292 const BatchedContext = /* */ 0b001;
293 -const RenderContext = /* */ 0b010;
293 +export const RenderContext = /* */ 0b010;
294 export const CommitContext = /* */ 0b100;
295
296 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+1 -1
@@ -290,7 +290,7 @@ type ExecutionContext = number;
290
291 export const NoContext = /* */ 0b000;
292 const BatchedContext = /* */ 0b001;
293 -const RenderContext = /* */ 0b010;
293 +export const RenderContext = /* */ 0b010;
294 export const CommitContext = /* */ 0b100;
295
296 type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+230 -1
@@ -1362,6 +1362,200 @@ describe('ReactOffscreen', () => {
1362
1363 expect(offscreenRef.current).not.toBeNull();
1364 });
1365 +
1366 + // @gate enableOffscreen
1367 + it('should lower update priority for detached Offscreen', async () => {
1368 + let updateChildState;
1369 + let updateHighPriorityComponentState;
1370 + let offscreenRef;
1371 +
1372 + function Child() {
1373 + const [state, _stateUpdate] = useState(0);
1374 + updateChildState = _stateUpdate;
1375 + const text = 'Child ' + state;
1376 + return <Text text={text} />;
1377 + }
1378 +
1379 + function HighPriorityComponent(props) {
1380 + const [state, _stateUpdate] = useState(0);
1381 + updateHighPriorityComponentState = _stateUpdate;
1382 + const text = 'HighPriorityComponent ' + state;
1383 + return (
1384 + <>
1385 + <Text text={text} />
1386 + {props.children}
1387 + </>
1388 + );
1389 + }
1390 +
1391 + function App() {
1392 + offscreenRef = useRef(null);
1393 + return (
1394 + <>
1395 + <HighPriorityComponent>
1396 + <Offscreen mode={'manual'} ref={offscreenRef}>
1397 + <Child />
1398 + </Offscreen>
1399 + </HighPriorityComponent>
1400 + </>
1401 + );
1402 + }
1403 +
1404 + const root = ReactNoop.createRoot();
1405 +
1406 + await act(async () => {
1407 + root.render(<App />);
1408 + });
1409 +
1410 + expect(Scheduler).toHaveYielded(['HighPriorityComponent 0', 'Child 0']);
1411 + expect(root).toMatchRenderedOutput(
1412 + <>
1413 + <span prop="HighPriorityComponent 0" />
1414 + <span prop="Child 0" />
1415 + </>,
1416 + );
1417 +
1418 + expect(offscreenRef.current).not.toBeNull();
1419 + expect(offscreenRef.current.detach).not.toBeNull();
1420 +
1421 + // Offscreen is attached by default. State updates from offscreen are **not defered**.
1422 + await act(async () => {
1423 + updateChildState(1);
1424 + updateHighPriorityComponentState(1);
1425 + expect(Scheduler).toFlushUntilNextPaint([
1426 + 'HighPriorityComponent 1',
1427 + 'Child 1',
1428 + ]);
1429 + expect(root).toMatchRenderedOutput(
1430 + <>
1431 + <span prop="HighPriorityComponent 1" />
1432 + <span prop="Child 1" />
1433 + </>,
1434 + );
1435 + });
1436 +
1437 + // detaching offscreen.
1438 + offscreenRef.current.detach();
1439 +
1440 + // Offscreen is detached. State updates from offscreen are **defered**.
1441 + await act(async () => {
1442 + updateChildState(2);
1443 + updateHighPriorityComponentState(2);
1444 + expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 2']);
1445 + expect(root).toMatchRenderedOutput(
1446 + <>
1447 + <span prop="HighPriorityComponent 2" />
1448 + <span prop="Child 1" />
1449 + </>,
1450 + );
1451 + });
1452 +
1453 + expect(Scheduler).toHaveYielded(['Child 2']);
1454 + expect(root).toMatchRenderedOutput(
1455 + <>
1456 + <span prop="HighPriorityComponent 2" />
1457 + <span prop="Child 2" />
1458 + </>,
1459 + );
1460 + });
1461 +
1462 + // @gate enableOffscreen
1463 + it('defers detachment if called during commit', async () => {
1464 + let updateChildState;
1465 + let updateHighPriorityComponentState;
1466 + let offscreenRef;
1467 + let nextRenderTriggerDetach = false;
1468 +
1469 + function Child() {
1470 + const [state, _stateUpdate] = useState(0);
1471 + updateChildState = _stateUpdate;
1472 + const text = 'Child ' + state;
1473 + return <Text text={text} />;
1474 + }
1475 +
1476 + function HighPriorityComponent(props) {
1477 + const [state, _stateUpdate] = useState(0);
1478 + updateHighPriorityComponentState = _stateUpdate;
1479 + const text = 'HighPriorityComponent ' + state;
1480 + useLayoutEffect(() => {
1481 + if (nextRenderTriggerDetach) {
1482 + offscreenRef.current.detach();
1483 + _stateUpdate(state + 1);
1484 + updateChildState(state + 1);
1485 + nextRenderTriggerDetach = false;
1486 + }
1487 + });
1488 + return (
1489 + <>
1490 + <Text text={text} />
1491 + {props.children}
1492 + </>
1493 + );
1494 + }
1495 +
1496 + function App() {
1497 + offscreenRef = useRef(null);
1498 + return (
1499 + <>
1500 + <HighPriorityComponent>
1501 + <Offscreen mode={'manual'} ref={offscreenRef}>
1502 + <Child />
1503 + </Offscreen>
1504 + </HighPriorityComponent>
1505 + </>
1506 + );
1507 + }
1508 +
1509 + const root = ReactNoop.createRoot();
1510 +
1511 + await act(async () => {
1512 + root.render(<App />);
1513 + });
1514 +
1515 + expect(Scheduler).toHaveYielded(['HighPriorityComponent 0', 'Child 0']);
1516 +
1517 + nextRenderTriggerDetach = true;
1518 +
1519 + // Offscreen is attached. State updates from offscreen are **not defered**.
1520 + // Offscreen is detached inside useLayoutEffect;
1521 + await act(async () => {
1522 + updateChildState(1);
1523 + updateHighPriorityComponentState(1);
1524 + expect(Scheduler).toFlushUntilNextPaint([
1525 + 'HighPriorityComponent 1',
1526 + 'Child 1',
1527 + 'HighPriorityComponent 2',
1528 + 'Child 2',
1529 + ]);
1530 + expect(root).toMatchRenderedOutput(
1531 + <>
1532 + <span prop="HighPriorityComponent 2" />
1533 + <span prop="Child 2" />
1534 + </>,
1535 + );
1536 + });
1537 +
1538 + // Offscreen is detached. State updates from offscreen are **defered**.
1539 + await act(async () => {
1540 + updateChildState(3);
1541 + updateHighPriorityComponentState(3);
1542 + expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 3']);
1543 + expect(root).toMatchRenderedOutput(
1544 + <>
1545 + <span prop="HighPriorityComponent 3" />
1546 + <span prop="Child 2" />
1547 + </>,
1548 + );
1549 + });
1550 +
1551 + expect(Scheduler).toHaveYielded(['Child 3']);
1552 + expect(root).toMatchRenderedOutput(
1553 + <>
1554 + <span prop="HighPriorityComponent 3" />
1555 + <span prop="Child 3" />
1556 + </>,
1557 + );
1558 + });
1559 });
1560
1561 // @gate enableOffscreen
@@ -1430,7 +1624,6 @@ describe('ReactOffscreen', () => {
1624 });
1625
1626 expect(offscreenRef.current).not.toBeNull();
1433 -
1627 await act(async () => {
1628 root.render(<App mode={'hidden'} />);
1629 });
@@ -1438,5 +1631,41 @@ describe('ReactOffscreen', () => {
1631 expect(offscreenRef.current).toBeNull();
1632 });
1633
1634 + // @gate enableOffscreen
1635 + it('should change _current', async () => {
1636 + let offscreenRef;
1637 + const root = ReactNoop.createRoot();
1638 +
1639 + function App({children}) {
1640 + offscreenRef = useRef(null);
1641 + return (
1642 + <Offscreen mode={'manual'} ref={offscreenRef}>
1643 + {children}
1644 + </Offscreen>
1645 + );
1646 + }
1647 +
1648 + await act(async () => {
1649 + root.render(
1650 + <App>
1651 + <div />
1652 + </App>,
1653 + );
1654 + });
1655 +
1656 + expect(offscreenRef.current).not.toBeNull();
1657 + const firstFiber = offscreenRef.current._current;
1658 +
1659 + await act(async () => {
1660 + root.render(
1661 + <App>
1662 + <span />
1663 + </App>,
1664 + );
1665 + });
1666 +
1667 + expect(offscreenRef.current._current === firstFiber).toBeFalsy();
1668 + });
1669 +
1670 // TODO: When attach/detach methods are implemented. Add tests for nested Offscreen case.
1671 });
scripts/error-codes/codes.json
+3 -2
@@ -440,5 +440,6 @@
440 "452": "React expected an <html> element (document.documentElement) to exist in the Document but one was not found. React never removes the documentElement for any Document it renders into so the cause is likely in some other script running on this page.",
441 "453": "React expected a <head> element (document.head) to exist in the Document but one was not found. React never removes the head for any Document it renders into so the cause is likely in some other script running on this page.",
442 "454": "React expected a <body> element (document.body) to exist in the Document but one was not found. React never removes the body for any Document it renders into so the cause is likely in some other script running on this page.",
443 - "455": "This CacheSignal was requested outside React which means that it is immediately aborted."
444 -}
\ No newline at end of file
443 + "455": "This CacheSignal was requested outside React which means that it is immediately aborted.",
444 + "456": "Calling Offscreen.detach before instance handle has been set."
445 +}