Compare commits
2 Commits
eslint-plu
...
offscreen-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0213dee8a | ||
|
|
eb6dc4d43f |
@@ -723,7 +723,11 @@ export function createFiberFromOffscreen(
|
||||
pendingMarkers: null,
|
||||
retryCache: null,
|
||||
transitions: null,
|
||||
detach: () => {},
|
||||
attach: () => {},
|
||||
_isDetached: false,
|
||||
};
|
||||
|
||||
fiber.stateNode = primaryChildInstance;
|
||||
return fiber;
|
||||
}
|
||||
@@ -744,6 +748,13 @@ export function createFiberFromLegacyHidden(
|
||||
pendingMarkers: null,
|
||||
transitions: null,
|
||||
retryCache: null,
|
||||
detach: () => {
|
||||
// noop
|
||||
},
|
||||
attach: () => {
|
||||
// noop
|
||||
},
|
||||
_isDetached: false,
|
||||
};
|
||||
fiber.stateNode = instance;
|
||||
return fiber;
|
||||
|
||||
@@ -723,7 +723,11 @@ export function createFiberFromOffscreen(
|
||||
pendingMarkers: null,
|
||||
retryCache: null,
|
||||
transitions: null,
|
||||
detach: () => {},
|
||||
attach: () => {},
|
||||
_isDetached: false,
|
||||
};
|
||||
|
||||
fiber.stateNode = primaryChildInstance;
|
||||
return fiber;
|
||||
}
|
||||
@@ -744,6 +748,13 @@ export function createFiberFromLegacyHidden(
|
||||
pendingMarkers: null,
|
||||
transitions: null,
|
||||
retryCache: null,
|
||||
detach: () => {
|
||||
// noop
|
||||
},
|
||||
attach: () => {
|
||||
// noop
|
||||
},
|
||||
_isDetached: false,
|
||||
};
|
||||
fiber.stateNode = instance;
|
||||
return fiber;
|
||||
|
||||
@@ -677,9 +677,13 @@ function updateOffscreenComponent(
|
||||
const prevState: OffscreenState | null =
|
||||
current !== null ? current.memoizedState : null;
|
||||
|
||||
markRef(current, workInProgress);
|
||||
|
||||
if (
|
||||
nextProps.mode === 'hidden' ||
|
||||
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
|
||||
(enableLegacyHidden &&
|
||||
nextProps.mode === 'unstable-defer-without-hiding') ||
|
||||
workInProgress.stateNode._isDetached
|
||||
) {
|
||||
// Rendering a hidden tree.
|
||||
|
||||
|
||||
@@ -677,9 +677,13 @@ function updateOffscreenComponent(
|
||||
const prevState: OffscreenState | null =
|
||||
current !== null ? current.memoizedState : null;
|
||||
|
||||
markRef(current, workInProgress);
|
||||
|
||||
if (
|
||||
nextProps.mode === 'hidden' ||
|
||||
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
|
||||
(enableLegacyHidden &&
|
||||
nextProps.mode === 'unstable-defer-without-hiding') ||
|
||||
workInProgress.stateNode._isDetached
|
||||
) {
|
||||
// Rendering a hidden tree.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
import type {Fiber} from './ReactInternalTypes';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {Lanes} from './ReactFiberLane.new';
|
||||
import {NoLanes} from './ReactFiberLane.new';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
|
||||
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
|
||||
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
|
||||
@@ -29,6 +30,7 @@ import type {
|
||||
} from './ReactFiberOffscreenComponent';
|
||||
import type {HookFlags} from './ReactHookEffectTags';
|
||||
import type {Cache} from './ReactFiberCacheComponent.new';
|
||||
import {scheduleMicrotask} from './ReactFiberHostConfig';
|
||||
import type {RootState} from './ReactFiberRoot.new';
|
||||
import type {
|
||||
Transition,
|
||||
@@ -154,6 +156,7 @@ import {
|
||||
setIsRunningInsertionEffect,
|
||||
getExecutionContext,
|
||||
CommitContext,
|
||||
RenderContext,
|
||||
NoContext,
|
||||
} from './ReactFiberWorkLoop.new';
|
||||
import {
|
||||
@@ -1078,7 +1081,9 @@ function commitLayoutEffectOnFiber(
|
||||
case OffscreenComponent: {
|
||||
const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
|
||||
if (isModernRoot) {
|
||||
const isHidden = finishedWork.memoizedState !== null;
|
||||
const isHidden =
|
||||
finishedWork.memoizedState !== null ||
|
||||
finishedWork.stateNode._isDetached;
|
||||
const newOffscreenSubtreeIsHidden =
|
||||
isHidden || offscreenSubtreeIsHidden;
|
||||
if (newOffscreenSubtreeIsHidden) {
|
||||
@@ -1116,6 +1121,14 @@ function commitLayoutEffectOnFiber(
|
||||
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
|
||||
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
|
||||
}
|
||||
|
||||
if (finishedWork.pendingProps.mode === null) {
|
||||
if (flags & Ref) {
|
||||
safelyAttachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
} else if (finishedWork.pendingProps.mode !== undefined) {
|
||||
safelyDetachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
} else {
|
||||
recursivelyTraverseLayoutEffects(
|
||||
finishedRoot,
|
||||
@@ -2247,6 +2260,30 @@ function getRetryCache(finishedWork) {
|
||||
}
|
||||
}
|
||||
|
||||
function attachOffscreenActions(offscreenFiber: Fiber, root: FiberRoot) {
|
||||
offscreenFiber.stateNode.detach = () => {
|
||||
const executionContext = getExecutionContext();
|
||||
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
|
||||
scheduleMicrotask(() => {
|
||||
offscreenFiber.stateNode._isDetached = true;
|
||||
disappearLayoutEffects(offscreenFiber);
|
||||
disconnectPassiveEffect(offscreenFiber);
|
||||
});
|
||||
} else {
|
||||
offscreenFiber.stateNode._isDetached = true;
|
||||
disappearLayoutEffects(offscreenFiber);
|
||||
disconnectPassiveEffect(offscreenFiber);
|
||||
}
|
||||
};
|
||||
|
||||
offscreenFiber.stateNode.attach = () => {
|
||||
// TODO: does not handle when attach is called from effect or when tree is rendered.
|
||||
offscreenFiber.stateNode._isDetached = false;
|
||||
reappearLayoutEffects(root, null, offscreenFiber, false);
|
||||
reconnectPassiveEffects(root, offscreenFiber, NoLanes, null, false);
|
||||
};
|
||||
}
|
||||
|
||||
function attachSuspenseRetryListeners(
|
||||
finishedWork: Fiber,
|
||||
wakeables: Set<Wakeable>,
|
||||
@@ -2625,6 +2662,7 @@ function commitMutationEffectsOnFiber(
|
||||
}
|
||||
|
||||
commitReconciliationEffects(finishedWork);
|
||||
attachOffscreenActions(finishedWork, root);
|
||||
|
||||
if (flags & Visibility) {
|
||||
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
|
||||
@@ -2651,7 +2689,7 @@ function commitMutationEffectsOnFiber(
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsMutation) {
|
||||
if (supportsMutation && offscreenInstance._isDetached !== true) {
|
||||
// TODO: This needs to run whenever there's an insertion or update
|
||||
// inside a hidden Offscreen tree.
|
||||
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
import type {Fiber} from './ReactInternalTypes';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {Lanes} from './ReactFiberLane.old';
|
||||
import {NoLanes} from './ReactFiberLane.old';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
|
||||
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
|
||||
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
|
||||
@@ -29,6 +30,7 @@ import type {
|
||||
} from './ReactFiberOffscreenComponent';
|
||||
import type {HookFlags} from './ReactHookEffectTags';
|
||||
import type {Cache} from './ReactFiberCacheComponent.old';
|
||||
import {scheduleMicrotask} from './ReactFiberHostConfig';
|
||||
import type {RootState} from './ReactFiberRoot.old';
|
||||
import type {
|
||||
Transition,
|
||||
@@ -154,6 +156,7 @@ import {
|
||||
setIsRunningInsertionEffect,
|
||||
getExecutionContext,
|
||||
CommitContext,
|
||||
RenderContext,
|
||||
NoContext,
|
||||
} from './ReactFiberWorkLoop.old';
|
||||
import {
|
||||
@@ -1078,7 +1081,9 @@ function commitLayoutEffectOnFiber(
|
||||
case OffscreenComponent: {
|
||||
const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
|
||||
if (isModernRoot) {
|
||||
const isHidden = finishedWork.memoizedState !== null;
|
||||
const isHidden =
|
||||
finishedWork.memoizedState !== null ||
|
||||
finishedWork.stateNode._isDetached;
|
||||
const newOffscreenSubtreeIsHidden =
|
||||
isHidden || offscreenSubtreeIsHidden;
|
||||
if (newOffscreenSubtreeIsHidden) {
|
||||
@@ -1116,6 +1121,14 @@ function commitLayoutEffectOnFiber(
|
||||
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
|
||||
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
|
||||
}
|
||||
|
||||
if (finishedWork.pendingProps.mode === null) {
|
||||
if (flags & Ref) {
|
||||
safelyAttachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
} else if (finishedWork.pendingProps.mode !== undefined) {
|
||||
safelyDetachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
} else {
|
||||
recursivelyTraverseLayoutEffects(
|
||||
finishedRoot,
|
||||
@@ -2247,6 +2260,30 @@ function getRetryCache(finishedWork) {
|
||||
}
|
||||
}
|
||||
|
||||
function attachOffscreenActions(offscreenFiber: Fiber, root: FiberRoot) {
|
||||
offscreenFiber.stateNode.detach = () => {
|
||||
const executionContext = getExecutionContext();
|
||||
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
|
||||
scheduleMicrotask(() => {
|
||||
offscreenFiber.stateNode._isDetached = true;
|
||||
disappearLayoutEffects(offscreenFiber);
|
||||
disconnectPassiveEffect(offscreenFiber);
|
||||
});
|
||||
} else {
|
||||
offscreenFiber.stateNode._isDetached = true;
|
||||
disappearLayoutEffects(offscreenFiber);
|
||||
disconnectPassiveEffect(offscreenFiber);
|
||||
}
|
||||
};
|
||||
|
||||
offscreenFiber.stateNode.attach = () => {
|
||||
// TODO: does not handle when attach is called from effect or when tree is rendered.
|
||||
offscreenFiber.stateNode._isDetached = false;
|
||||
reappearLayoutEffects(root, null, offscreenFiber, false);
|
||||
reconnectPassiveEffects(root, offscreenFiber, NoLanes, null, false);
|
||||
};
|
||||
}
|
||||
|
||||
function attachSuspenseRetryListeners(
|
||||
finishedWork: Fiber,
|
||||
wakeables: Set<Wakeable>,
|
||||
@@ -2625,6 +2662,7 @@ function commitMutationEffectsOnFiber(
|
||||
}
|
||||
|
||||
commitReconciliationEffects(finishedWork);
|
||||
attachOffscreenActions(finishedWork, root);
|
||||
|
||||
if (flags & Visibility) {
|
||||
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
|
||||
@@ -2651,7 +2689,7 @@ function commitMutationEffectsOnFiber(
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsMutation) {
|
||||
if (supportsMutation && offscreenInstance._isDetached !== true) {
|
||||
// TODO: This needs to run whenever there's an insertion or update
|
||||
// inside a hidden Offscreen tree.
|
||||
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
|
||||
|
||||
@@ -302,7 +302,6 @@ if (supportsMutation) {
|
||||
};
|
||||
} else if (supportsPersistence) {
|
||||
// Persistent host tree mode
|
||||
|
||||
appendAllChildren = function(
|
||||
parent: Instance,
|
||||
workInProgress: Fiber,
|
||||
@@ -410,7 +409,14 @@ if (supportsMutation) {
|
||||
if (child !== null) {
|
||||
child.return = node;
|
||||
}
|
||||
appendAllChildrenToContainer(containerChildSet, node, true, true);
|
||||
// Detached tree is hidden from user space.
|
||||
const _needsVisibilityToggle = node.stateNode._isDetached === false;
|
||||
appendAllChildrenToContainer(
|
||||
containerChildSet,
|
||||
node,
|
||||
_needsVisibilityToggle,
|
||||
true,
|
||||
);
|
||||
} else if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
|
||||
@@ -302,7 +302,6 @@ if (supportsMutation) {
|
||||
};
|
||||
} else if (supportsPersistence) {
|
||||
// Persistent host tree mode
|
||||
|
||||
appendAllChildren = function(
|
||||
parent: Instance,
|
||||
workInProgress: Fiber,
|
||||
@@ -410,7 +409,14 @@ if (supportsMutation) {
|
||||
if (child !== null) {
|
||||
child.return = node;
|
||||
}
|
||||
appendAllChildrenToContainer(containerChildSet, node, true, true);
|
||||
// Detached tree is hidden from user space.
|
||||
const _needsVisibilityToggle = node.stateNode._isDetached === false;
|
||||
appendAllChildrenToContainer(
|
||||
containerChildSet,
|
||||
node,
|
||||
_needsVisibilityToggle,
|
||||
true,
|
||||
);
|
||||
} else if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
|
||||
@@ -52,4 +52,7 @@ export type OffscreenInstance = {
|
||||
pendingMarkers: Set<TracingMarkerInstance> | null,
|
||||
transitions: Set<Transition> | null,
|
||||
retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
|
||||
detach: () => void,
|
||||
attach: () => void,
|
||||
_isDetached: boolean,
|
||||
};
|
||||
|
||||
@@ -277,7 +277,7 @@ type ExecutionContext = number;
|
||||
|
||||
export const NoContext = /* */ 0b000;
|
||||
const BatchedContext = /* */ 0b001;
|
||||
const RenderContext = /* */ 0b010;
|
||||
export const RenderContext = /* */ 0b010;
|
||||
export const CommitContext = /* */ 0b100;
|
||||
|
||||
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
@@ -277,7 +277,7 @@ type ExecutionContext = number;
|
||||
|
||||
export const NoContext = /* */ 0b000;
|
||||
const BatchedContext = /* */ 0b001;
|
||||
const RenderContext = /* */ 0b010;
|
||||
export const RenderContext = /* */ 0b010;
|
||||
export const CommitContext = /* */ 0b100;
|
||||
|
||||
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
65
packages/react-reconciler/src/__tests__/ReactOffscreen-DOM-test.js
vendored
Normal file
65
packages/react-reconciler/src/__tests__/ReactOffscreen-DOM-test.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
let React;
|
||||
let ReactDOMClient;
|
||||
let Offscreen;
|
||||
let container;
|
||||
let act;
|
||||
let useRef;
|
||||
|
||||
describe('ReactOffscreen', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
React = require('react');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
Offscreen = React.unstable_Offscreen;
|
||||
act = require('jest-react').act;
|
||||
useRef = React.useRef;
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
// @gate enableOffscreen
|
||||
xit('does not attach event handlers by default', async () => {
|
||||
const onClick = jest.fn();
|
||||
let offscreenRef;
|
||||
|
||||
function App({mode}) {
|
||||
offscreenRef = useRef(null);
|
||||
return (
|
||||
<Offscreen ref={offscreenRef} mode={null}>
|
||||
<span id="span-1" onClick={onClick} />
|
||||
</Offscreen>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<App mode={'visible'} />);
|
||||
});
|
||||
|
||||
function click() {
|
||||
container
|
||||
.querySelector('#span-1')
|
||||
.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, cancelable: true}),
|
||||
);
|
||||
}
|
||||
|
||||
expect(offscreenRef.current).not.toBeNull();
|
||||
|
||||
click();
|
||||
|
||||
expect(onClick.mock.calls.length).toBe(1);
|
||||
|
||||
offscreenRef.current.detach();
|
||||
|
||||
click();
|
||||
|
||||
expect(onClick.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ let act;
|
||||
let LegacyHidden;
|
||||
let Offscreen;
|
||||
let useState;
|
||||
let useRef;
|
||||
let useLayoutEffect;
|
||||
let useEffect;
|
||||
let useMemo;
|
||||
@@ -24,6 +25,7 @@ describe('ReactOffscreen', () => {
|
||||
useLayoutEffect = React.useLayoutEffect;
|
||||
useEffect = React.useEffect;
|
||||
useMemo = React.useMemo;
|
||||
useRef = React.useRef;
|
||||
startTransition = React.startTransition;
|
||||
});
|
||||
|
||||
@@ -1259,4 +1261,371 @@ describe('ReactOffscreen', () => {
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableOffscreen
|
||||
it('defers updates in hidden tree', async () => {
|
||||
let updateChildState;
|
||||
let updateHighPriorityComponentState;
|
||||
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
// Mount hidden tree.
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<>
|
||||
<HighPriorityComponent>
|
||||
<Offscreen mode="hidden">
|
||||
<Child />
|
||||
</Offscreen>
|
||||
</HighPriorityComponent>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded(['HighPriorityComponent 0', 'Child 0']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 0" />
|
||||
<span hidden={true} prop="Child 0" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
updateChildState(1);
|
||||
updateHighPriorityComponentState(1);
|
||||
expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 1']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 1" />
|
||||
<span hidden={true} prop="Child 0" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Child 1']);
|
||||
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 1" />
|
||||
<span hidden={true} prop="Child 1" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
describe('manual interactivity', () => {
|
||||
// @gate enableOffscreen
|
||||
it('should attach ref only for mode null', async () => {
|
||||
let offscreenRef;
|
||||
|
||||
function App({mode}) {
|
||||
offscreenRef = useRef(null);
|
||||
return (
|
||||
<Offscreen
|
||||
mode={mode}
|
||||
ref={ref => {
|
||||
offscreenRef.current = ref;
|
||||
}}>
|
||||
<div />
|
||||
</Offscreen>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App mode={null} />);
|
||||
});
|
||||
|
||||
expect(offscreenRef.current).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App mode={'visible'} />);
|
||||
});
|
||||
|
||||
expect(offscreenRef.current).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App mode={'hidden'} />);
|
||||
});
|
||||
|
||||
expect(offscreenRef.current).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App mode={null} />);
|
||||
});
|
||||
|
||||
expect(offscreenRef.current).not.toBeNull();
|
||||
});
|
||||
|
||||
// @gate enableOffscreen
|
||||
it('should lower update priority for detached Offscreen', 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>
|
||||
<Offscreen mode={null} ref={offscreenRef}>
|
||||
<Child />
|
||||
</Offscreen>
|
||||
</HighPriorityComponent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded(['HighPriorityComponent 0', 'Child 0']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 0" />
|
||||
<span prop="Child 0" />
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(offscreenRef.current).not.toBeNull();
|
||||
expect(offscreenRef.current.detach).not.toBeNull();
|
||||
|
||||
// Offscreen is attached by default. State updates from offscreen are **not defered**.
|
||||
await act(async () => {
|
||||
updateChildState(1);
|
||||
updateHighPriorityComponentState(1);
|
||||
expect(Scheduler).toFlushUntilNextPaint([
|
||||
'HighPriorityComponent 1',
|
||||
'Child 1',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 1" />
|
||||
<span prop="Child 1" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// detaching offscreen.
|
||||
offscreenRef.current.detach();
|
||||
|
||||
// Offscreen is detached. State updates from offscreen are **defered**.
|
||||
await act(async () => {
|
||||
updateChildState(2);
|
||||
updateHighPriorityComponentState(2);
|
||||
expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 2']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 2" />
|
||||
<span prop="Child 1" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Child 2']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 2" />
|
||||
<span prop="Child 2" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableOffscreen
|
||||
it('defers detachment if called during commit', async () => {
|
||||
let updateChildState;
|
||||
let updateHighPriorityComponentState;
|
||||
let offscreenRef;
|
||||
let nextRenderTriggerDetach = 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) {
|
||||
offscreenRef.current.detach();
|
||||
_stateUpdate(state + 1);
|
||||
updateChildState(state + 1);
|
||||
nextRenderTriggerDetach = false;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Text text={text} />
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
offscreenRef = useRef(null);
|
||||
return (
|
||||
<>
|
||||
<HighPriorityComponent>
|
||||
<Offscreen mode={null} ref={offscreenRef}>
|
||||
<Child />
|
||||
</Offscreen>
|
||||
</HighPriorityComponent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded(['HighPriorityComponent 0', 'Child 0']);
|
||||
|
||||
nextRenderTriggerDetach = true;
|
||||
|
||||
// Offscreen is attached. State updates from offscreen are **not defered**.
|
||||
// Offscreen is detached inside useLayoutEffect;
|
||||
await act(async () => {
|
||||
updateChildState(1);
|
||||
updateHighPriorityComponentState(1);
|
||||
expect(Scheduler).toFlushUntilNextPaint([
|
||||
'HighPriorityComponent 1',
|
||||
'Child 1',
|
||||
'HighPriorityComponent 2',
|
||||
'Child 2',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 2" />
|
||||
<span prop="Child 2" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// Offscreen is detached. State updates from offscreen are **defered**.
|
||||
await act(async () => {
|
||||
updateChildState(3);
|
||||
updateHighPriorityComponentState(3);
|
||||
expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 3']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 3" />
|
||||
<span prop="Child 2" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Child 3']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="HighPriorityComponent 3" />
|
||||
<span prop="Child 3" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableOffscreen
|
||||
it('does not mount tree until attach is called', async () => {
|
||||
let offscreenRef;
|
||||
let spanRef;
|
||||
|
||||
function Child() {
|
||||
spanRef = useRef(null);
|
||||
useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Mount Child');
|
||||
return () => {
|
||||
Scheduler.unstable_yieldValue('Unmount Child');
|
||||
};
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Mount Layout Child');
|
||||
return () => {
|
||||
Scheduler.unstable_yieldValue('Unmount Layout Child');
|
||||
};
|
||||
});
|
||||
|
||||
return <span ref={spanRef}>Child</span>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Offscreen mode={null} ref={el => (offscreenRef = el)}>
|
||||
<Child />
|
||||
</Offscreen>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
});
|
||||
|
||||
expect(offscreenRef).not.toBeNull();
|
||||
expect(spanRef.current).not.toBeNull();
|
||||
expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']);
|
||||
|
||||
offscreenRef.detach();
|
||||
|
||||
expect(spanRef.current).toBeNull();
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Unmount Layout Child',
|
||||
'Unmount Child',
|
||||
]);
|
||||
|
||||
offscreenRef.attach();
|
||||
|
||||
expect(spanRef.current).not.toBeNull();
|
||||
expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user