Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[flags] Add `enableParallelTransitions` (#35392) · react/react@6913ea4 · GitHub
Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [flags] Add `enableParallelTransitions` (#35392) · react/react@6913ea4 · GitHub
Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [flags] Add `enableParallelTransitions` (#35392) · react/react@6913ea4 · GitHub
Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [flags] Add `enableParallelTransitions` (#35392) · react/react@6913ea4 · GitHub
Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [flags] Add `enableParallelTransitions` (#35392) · react/react@6913ea4 · GitHub
Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [flags] Add `enableParallelTransitions` (#35392) · react/react@6913ea4 · GitHub
Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [flags] Add `enableParallelTransitions` (#35392) · react/react@6913ea4 · GitHub
Skip to content

Commit 6913ea4

Browse files
authored
[flags] Add enableParallelTransitions (#35392)
## Overview Adds a feature flag `enableParallelTransitions` to experiment with engantling transitions less often. ## Motivation Currently we over-entangle transition lanes. It's a common misunderstanding that React entangles all transitions, always. We actually will complete transitions independently in many cases. For example, [this codepen](https://codepen.io/GabbeV/pen/pvyKBrM) from [@GabbeV](https://bsky.app/profile/gabbev.bsky.social/post/3m6uq2abihk2x) shows transitions completing independently. However, in many cases we entangle when we don't need to, instead of letting the independent transitons complete independently. We still want to entangle for updates that happen on the same queue. ## Example As an example of what this flag would change, consider two independent counter components: ```js function Counter({ label }) { const [count, setCount] = useState(0); return ( <div> <span>{use(readCache(`${label} ${count}`))} </span> <Button action={() => { setCount((c) => c + 1); }} > Next {label} </Button> </div> ); } ``` ```js export default function App() { return ( <> <Counter label="A" /> <Counter label="B" /> </> ); } ``` ### Before The behavior today is to entange them, meaning they always commit together: https://github.com/user-attachments/assets/adead60e-8a98-4a20-a440-1efdf85b2142 ### After In this experiment, they will complete independently (if they don't depend on each other): https://github.com/user-attachments/assets/181632b5-3c92-4a29-a571-3637f3fab8cd ## Early Research This change is in early research, and is not in the experimental channel. We're going to experiment with this at Meta to understand how much of a breaking change, and how beneficial it is before commiting to shipping it in experimental and beyond.
1 parent cf993fb commit 6913ea4

12 files changed

Lines changed: 517 additions & 9 deletions

‎packages/react-reconciler/src/ReactFiberLane.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
disableLegacyMode,
3030
enableDefaultTransitionIndicator,
3131
enableGestureTransition,
32+
enableParallelTransitions,
3233
}from'shared/ReactFeatureFlags';
3334
import{isDevToolsPresent}from'./ReactFiberDevToolsHook';
3435
import{clz32}from'./clz32';
@@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
208209
caseTransitionLane8:
209210
caseTransitionLane9:
210211
caseTransitionLane10:
212+
if(enableParallelTransitions){
213+
returngetHighestPriorityLane(lanes);
214+
}
211215
returnlanes&TransitionUpdateLanes;
212216
caseTransitionLane11:
213217
caseTransitionLane12:

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
enableViewTransition,
5959
enableGestureTransition,
6060
enableDefaultTransitionIndicator,
61+
enableParallelTransitions,
6162
}from'shared/ReactFeatureFlags';
6263
import{resetOwnerStackLimit}from'shared/ReactOwnerStackReset';
6364
importReactSharedInternalsfrom'shared/ReactSharedInternals';
@@ -1777,6 +1778,11 @@ function markRootSuspended(
17771778
spawnedLane: Lane,
17781779
didAttemptEntireTree: boolean,
17791780
){
1781+
if(enableParallelTransitions){
1782+
// When suspending, we should always mark the entangled lanes as suspended.
1783+
suspendedLanes=getEntangledLanes(root,suspendedLanes);
1784+
}
1785+
17801786
// When suspending, we should always exclude lanes that were pinged or (more
17811787
// rarely, since we try to avoid it) updated during the render phase.
17821788
suspendedLanes=removeLanes(suspendedLanes,workInProgressRootPingedLanes);

‎packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js‎

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => {
420420
// The initial value suspended, so we attempt the final value, which
421421
// also suspends.
422422
'Suspend! [Final]',
423-
// pre-warming
424-
'Suspend! [Loading...]',
425-
'Suspend! [Final]',
423+
...(gate('enableParallelTransitions')
424+
? []
425+
: [
426+
// Existing bug: Unnecessary pre-warm.
427+
'Suspend! [Loading...]',
428+
'Suspend! [Final]',
429+
]),
426430
]);
427431
expect(root).toMatchRenderedOutput(null);
428432

@@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => {
439443
},
440444
);
441445

446+
it(
447+
'if a suspended render spawns a deferred task that suspends on a sibling, '+
448+
'we can finish the original task if the original sibling loads first',
449+
async()=>{
450+
functionApp(){
451+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
452+
return(
453+
<>
454+
<AsyncTexttext={deferredText}/>{' '}
455+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
456+
</>
457+
);
458+
}
459+
460+
constroot=ReactNoop.createRoot();
461+
awaitact(()=>root.render(<Apptext="a"/>));
462+
assertLog([
463+
'Suspend! [Loading...]',
464+
// The initial value suspended, so we attempt the final value, which
465+
// also suspends.
466+
'Suspend! [Final]',
467+
'Suspend! [Sibling: Final]',
468+
...(gate('enableParallelTransitions')
469+
? [
470+
// With parallel transitions,
471+
// we do not continue pre-warming.
472+
]
473+
: [
474+
'Suspend! [Loading...]',
475+
'Suspend! [Sibling: Loading...]',
476+
'Suspend! [Final]',
477+
'Suspend! [Sibling: Final]',
478+
]),
479+
]);
480+
expect(root).toMatchRenderedOutput(null);
481+
482+
// The final value loads, so we can skip the initial value entirely.
483+
awaitact(()=>{
484+
resolveText('Final');
485+
});
486+
assertLog(['Final','Suspend! [Sibling: Final]']);
487+
expect(root).toMatchRenderedOutput(null);
488+
489+
// The initial value resolves first, so we render that.
490+
awaitact(()=>resolveText('Loading...'));
491+
assertLog([
492+
'Loading...',
493+
'Suspend! [Sibling: Loading...]',
494+
'Final',
495+
'Suspend! [Sibling: Final]',
496+
...(gate('enableParallelTransitions')
497+
? [
498+
// With parallel transitions,
499+
// we do not continue pre-warming.
500+
]
501+
: [
502+
'Loading...',
503+
'Suspend! [Sibling: Loading...]',
504+
'Final',
505+
'Suspend! [Sibling: Final]',
506+
]),
507+
]);
508+
expect(root).toMatchRenderedOutput(null);
509+
510+
// The Final sibling loads, we're unblocked and commit.
511+
awaitact(()=>{
512+
resolveText('Sibling: Final');
513+
});
514+
assertLog(['Final','Sibling: Final']);
515+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
516+
517+
// We already rendered the Final value, so nothing happens
518+
awaitact(()=>{
519+
resolveText('Sibling: Loading...');
520+
});
521+
assertLog([]);
522+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
523+
},
524+
);
525+
526+
it(
527+
'if a suspended render spawns a deferred task that suspends on a sibling,'+
528+
' we can switch to the deferred task without finishing the original one',
529+
async()=>{
530+
functionApp(){
531+
constdeferredText=useDeferredValue(`Final`,`Loading...`);
532+
return(
533+
<>
534+
<AsyncTexttext={deferredText}/>{' '}
535+
<AsyncTexttext={`Sibling: ${deferredText}`}/>
536+
</>
537+
);
538+
}
539+
540+
constroot=ReactNoop.createRoot();
541+
awaitact(()=>root.render(<Apptext="a"/>));
542+
assertLog([
543+
'Suspend! [Loading...]',
544+
// The initial value suspended, so we attempt the final value, which
545+
// also suspends.
546+
'Suspend! [Final]',
547+
'Suspend! [Sibling: Final]',
548+
...(gate('enableParallelTransitions')
549+
? [
550+
// With parallel transitions,
551+
// we do not continue pre-warming.
552+
]
553+
: [
554+
'Suspend! [Loading...]',
555+
'Suspend! [Sibling: Loading...]',
556+
'Suspend! [Final]',
557+
'Suspend! [Sibling: Final]',
558+
]),
559+
]);
560+
expect(root).toMatchRenderedOutput(null);
561+
562+
// The final value loads, so we can skip the initial value entirely.
563+
awaitact(()=>{
564+
resolveText('Final');
565+
});
566+
assertLog(['Final','Suspend! [Sibling: Final]']);
567+
expect(root).toMatchRenderedOutput(null);
568+
569+
// The initial value resolves first, so we render that.
570+
awaitact(()=>resolveText('Loading...'));
571+
assertLog([
572+
'Loading...',
573+
'Suspend! [Sibling: Loading...]',
574+
'Final',
575+
'Suspend! [Sibling: Final]',
576+
...(gate('enableParallelTransitions')
577+
? [
578+
// With parallel transitions,
579+
// we do not continue pre-warming.
580+
]
581+
: [
582+
'Loading...',
583+
'Suspend! [Sibling: Loading...]',
584+
'Final',
585+
'Suspend! [Sibling: Final]',
586+
]),
587+
]);
588+
expect(root).toMatchRenderedOutput(null);
589+
590+
// The initial sibling loads, we're unblocked and commit.
591+
awaitact(()=>{
592+
resolveText('Sibling: Loading...');
593+
});
594+
assertLog([
595+
'Loading...',
596+
'Sibling: Loading...',
597+
'Final',
598+
'Suspend! [Sibling: Final]',
599+
]);
600+
expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601+
602+
// Now unblock the final sibling.
603+
awaitact(()=>{
604+
resolveText('Sibling: Final');
605+
});
606+
assertLog(['Final','Sibling: Final']);
607+
expect(root).toMatchRenderedOutput('Final Sibling: Final');
608+
},
609+
);
610+
442611
it(
443612
'if a suspended render spawns a deferred task, we can switch to the '+
444613
'deferred task without finishing the original one (no Suspense boundary, '+
@@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => {
462631
// The initial value suspended, so we attempt the final value, which
463632
// also suspends.
464633
'Suspend! [Final]',
465-
// pre-warming
466-
'Suspend! [Loading...]',
467-
'Suspend! [Final]',
634+
...(gate('enableParallelTransitions')
635+
? [
636+
// With parallel transitions,
637+
// we do not continue pre-warming.
638+
]
639+
: ['Suspend! [Loading...]','Suspend! [Final]']),
468640
]);
469641
expect(root).toMatchRenderedOutput(null);
470642

@@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => {
539711
// The initial value suspended, so we attempt the final value, which
540712
// also suspends.
541713
'Suspend! [Final]',
542-
// pre-warming
543-
'Suspend! [Loading...]',
544-
'Suspend! [Final]',
714+
...(gate('enableParallelTransitions')
715+
? [
716+
// With parallel transitions,
717+
// we do not continue pre-warming.
718+
]
719+
: ['Suspend! [Loading...]','Suspend! [Final]']),
545720
]);
546721
expect(root).toMatchRenderedOutput(null);
547722

0 commit comments

Comments
 (0)