Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
disableLegacyMode,
enableDefaultTransitionIndicator,
enableGestureTransition,
enableParallelTransitions,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {clz32} from './clz32';
Expand Down Expand Up @@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
case TransitionLane8:
case TransitionLane9:
case TransitionLane10:
if (enableParallelTransitions) {
return getHighestPriorityLane(lanes);
}
return lanes & TransitionUpdateLanes;
case TransitionLane11:
case TransitionLane12:
Expand Down
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
enableViewTransition,
enableGestureTransition,
enableDefaultTransitionIndicator,
enableParallelTransitions,
} from 'shared/ReactFeatureFlags';
import {resetOwnerStackLimit} from 'shared/ReactOwnerStackReset';
import ReactSharedInternals from 'shared/ReactSharedInternals';
Expand Down Expand Up @@ -1719,6 +1720,11 @@ function markRootSuspended(
spawnedLane: Lane,
didAttemptEntireTree: boolean,
) {
if (enableParallelTransitions) {
// When suspending, we should always mark the entangled lanes as suspended.
suspendedLanes = getEntangledLanes(root, suspendedLanes);
}

// When suspending, we should always exclude lanes that were pinged or (more
// rarely, since we try to avoid it) updated during the render phase.
suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes);
Expand Down
18 changes: 12 additions & 6 deletions packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,10 @@ describe('ReactDeferredValue', () => {
// also suspends.
'Suspend! [Final]',
// pre-warming
'Suspend! [Loading...]',
'Suspend! [Final]',
...(gate('enableParallelTransitions')
? // TODO: this loses pre-warming, maybe that's fine?
[]
: ['Suspend! [Loading...]', 'Suspend! [Final]']),
]);
expect(root).toMatchRenderedOutput(null);

Expand Down Expand Up @@ -463,8 +465,10 @@ describe('ReactDeferredValue', () => {
// also suspends.
'Suspend! [Final]',
// pre-warming
'Suspend! [Loading...]',
'Suspend! [Final]',
...(gate('enableParallelTransitions')
? // TODO: this loses pre-warming, maybe that's fine?
[]
: ['Suspend! [Loading...]', 'Suspend! [Final]']),
]);
expect(root).toMatchRenderedOutput(null);

Expand Down Expand Up @@ -540,8 +544,10 @@ describe('ReactDeferredValue', () => {
// also suspends.
'Suspend! [Final]',
// pre-warming
'Suspend! [Loading...]',
'Suspend! [Final]',
...(gate('enableParallelTransitions')
? // TODO: this loses pre-warming, maybe that's fine?
[]
: ['Suspend! [Loading...]', 'Suspend! [Final]']),
]);
expect(root).toMatchRenderedOutput(null);

Expand Down
313 changes: 313 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactTransition-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,319 @@ describe('ReactTransition', () => {
expect(root).toMatchRenderedOutput('Async');
});

// @gate enableLegacyCache
it('when multiple transitions update different queues, they entangle', async () => {
let setA;
let startTransitionA;
let setB;
let startTransitionB;
function A() {
const [a, _setA] = useState(0);
const [isPending, _startTransitionA] = useTransition();
setA = _setA;
startTransitionA = _startTransitionA;

return (
<span>
{isPending && (
<span>
<Text text="Pending A..." />
</span>
)}
<AsyncText text={`A: ${a}`} />
</span>
);
}

function B() {
const [b, _setB] = useState(0);
const [isPending, _startTransitionB] = useTransition();
setB = _setB;
startTransitionB = _startTransitionB;

return (
<span>
{isPending && (
<span>
<Text text="Pending B..." />
</span>
)}
<AsyncText text={`B: ${b}`} />
</span>
);
}
function App() {
return (
<>
<Suspense fallback={<span>Loading A</span>}>
<A />
</Suspense>
<Suspense fallback={<span>Loading B</span>}>
<B />
</Suspense>
</>
);
}

// Initial render
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog([
'Suspend! [A: 0]',
'Suspend! [B: 0]',
'Suspend! [A: 0]',
'Suspend! [B: 0]',
]);
expect(root).toMatchRenderedOutput(
<>
<span>Loading A</span>
<span>Loading B</span>
</>,
);

// Resolve
await act(() => {
resolveText('A: 0');
resolveText('B: 0');
});
assertLog(['A: 0', 'B: 0']);
expect(root).toMatchRenderedOutput(
<>
<span>A: 0</span>
<span>B: 0</span>
</>,
);

// Start transitioning A
await act(() => {
startTransitionA(() => {
setA(1);
});
});
assertLog(['Pending A...', 'A: 0', 'Suspend! [A: 1]']);
expect(root).toMatchRenderedOutput(
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>B: 0</span>
</>,
);

// Start transitioning B
await act(() => {
startTransitionB(() => {
setB(1);
});
});
assertLog(['Pending B...', 'B: 0', 'Suspend! [A: 1]', 'Suspend! [B: 1]']);
expect(root).toMatchRenderedOutput(
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>
<span>Pending B...</span>B: 0
</span>
</>,
);

// Resolve B
await act(() => {
resolveText('B: 1');
});
assertLog(
gate('enableParallelTransitions')
? ['B: 1', 'Suspend! [A: 1]']
: ['Suspend! [A: 1]', 'B: 1'],
);
expect(root).toMatchRenderedOutput(
gate('enableParallelTransitions') ? (
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>B: 1</span>
</>
) : (
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>
<span>Pending B...</span>B: 0
</span>
</>
),
);

// Resolve A
await act(() => {
resolveText('A: 1');
});
assertLog(gate('enableParallelTransitions') ? ['A: 1'] : ['A: 1', 'B: 1']);
expect(root).toMatchRenderedOutput(
<>
<span>A: 1</span>
<span>B: 1</span>
</>,
);
});

// @gate enableLegacyCache
it('when multiple transitions update different queues, but suspend the same boundary, they do entangle', async () => {
let setA;
let startTransitionA;
let setB;
let startTransitionB;
function A() {
const [a, _setA] = useState(0);
const [isPending, _startTransitionA] = useTransition();
setA = _setA;
startTransitionA = _startTransitionA;

return (
<span>
{isPending && (
<span>
<Text text="Pending A..." />
</span>
)}
<AsyncText text={`A: ${a}`} />
</span>
);
}

function B() {
const [b, _setB] = useState(0);
const [isPending, _startTransitionB] = useTransition();
setB = _setB;
startTransitionB = _startTransitionB;

return (
<span>
{isPending && (
<span>
<Text text="Pending B..." />
</span>
)}
<AsyncText text={`B: ${b}`} />
</span>
);
}
function App() {
return (
<Suspense fallback={<span>Loading...</span>}>
<A />
<B />
</Suspense>
);
}

// Initial render
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog([
'Suspend! [A: 0]',
// pre-warming
'Suspend! [A: 0]',
'Suspend! [B: 0]',
]);
expect(root).toMatchRenderedOutput(<span>Loading...</span>);

// Resolve
await act(() => {
resolveText('A: 0');
resolveText('B: 0');
});
assertLog(['A: 0', 'B: 0']);
expect(root).toMatchRenderedOutput(
<>
<span>A: 0</span>
<span>B: 0</span>
</>,
);

// Start transitioning A
await act(() => {
startTransitionA(() => {
setA(1);
});
});
assertLog(['Pending A...', 'A: 0', 'Suspend! [A: 1]']);
expect(root).toMatchRenderedOutput(
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>B: 0</span>
</>,
);

// Start transitioning B
await act(() => {
startTransitionB(() => {
setB(1);
});
});
assertLog(['Pending B...', 'B: 0', 'Suspend! [A: 1]', 'Suspend! [B: 1]']);
expect(root).toMatchRenderedOutput(
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>
<span>Pending B...</span>B: 0
</span>
</>,
);

// Resolve B
await act(() => {
resolveText('B: 1');
});
assertLog(
gate('enableParallelTransitions')
? ['B: 1', 'Suspend! [A: 1]']
: ['Suspend! [A: 1]', 'B: 1'],
);
expect(root).toMatchRenderedOutput(
gate('enableParallelTransitions') ? (
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>B: 1</span>
</>
) : (
<>
<span>
<span>Pending A...</span>A: 0
</span>
<span>
<span>Pending B...</span>B: 0
</span>
</>
),
);

// Resolve A
await act(() => {
resolveText('A: 1');
});
assertLog(gate('enableParallelTransitions') ? ['A: 1'] : ['A: 1', 'B: 1']);
expect(root).toMatchRenderedOutput(
<>
<span>A: 1</span>
<span>B: 1</span>
</>,
);
});

// @gate enableLegacyCache
it(
'when multiple transitions update the same queue, only the most recent ' +
Expand Down
Loading
Loading