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
+}