Skip to content

Commit ffb7566

Browse files
committed
Synchronously render the transition lane on popstate
1 parent ee85098 commit ffb7566

File tree

12 files changed

+140
-9
lines changed

12 files changed

+140
-9
lines changed

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ export function getCurrentEventPriority() {
346346
return DefaultEventPriority;
347347
}
348348

349+
export function getIsCurrentEventPopState() {
350+
return false;
351+
}
352+
349353
// The ART renderer is secondary to the React DOM renderer.
350354
export const isPrimaryRenderer = false;
351355

packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,10 @@ export function getCurrentEventPriority(): EventPriority {
388388
return getEventPriority(currentEvent.type);
389389
}
390390

391+
export function getIsCurrentEventPopState(): boolean {
392+
return window.event && window.event.type === 'popstate';
393+
}
394+
391395
export const isPrimaryRenderer = true;
392396
export const warnsIfNotActing = true;
393397
// This initialization code may run even on server environments

packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,4 +658,84 @@ describe('ReactDOMFiberAsync', () => {
658658

659659
expect(container.textContent).toBe('new');
660660
});
661+
662+
it('should synchronously render the transition lane in a popState', async () => {
663+
function App() {
664+
const [syncState, setSyncState] = React.useState(false);
665+
const [hasNavigated, setHasNavigated] = React.useState(false);
666+
function onPopstate() {
667+
Scheduler.unstable_yieldValue(`popState`);
668+
setSyncState(true);
669+
React.startTransition(() => {
670+
setHasNavigated(true);
671+
});
672+
}
673+
React.useEffect(() => {
674+
window.addEventListener('popstate', onPopstate);
675+
return () => window.removeEventListener('popstate', onPopstate);
676+
}, []);
677+
Scheduler.unstable_yieldValue(`render:${hasNavigated}/${syncState}`);
678+
return null;
679+
}
680+
const root = ReactDOMClient.createRoot(container);
681+
await act(async () => {
682+
root.render(<App />);
683+
});
684+
expect(Scheduler).toHaveYielded(['render:false/false']);
685+
686+
await act(async () => {
687+
const popStateEvent = new Event('popstate');
688+
window.dispatchEvent(popStateEvent);
689+
});
690+
expect(Scheduler).toHaveYielded(['popState', 'render:true/true']);
691+
692+
root.unmount();
693+
});
694+
695+
it('transition lane in popState should still yield if it suspends', async () => {
696+
const never = {then() {}};
697+
let _setText;
698+
699+
function App() {
700+
const [shouldSuspend, setShouldSuspend] = React.useState(false);
701+
const [text, setText] = React.useState('0');
702+
_setText = setText;
703+
if (shouldSuspend) {
704+
Scheduler.unstable_yieldValue('Suspend!');
705+
throw never;
706+
}
707+
function onPopstate() {
708+
Scheduler.unstable_yieldValue(`popState`);
709+
React.startTransition(() => {
710+
setShouldSuspend(val => !val);
711+
});
712+
}
713+
React.useEffect(() => {
714+
window.addEventListener('popstate', onPopstate);
715+
return () => window.removeEventListener('popstate', onPopstate);
716+
}, []);
717+
Scheduler.unstable_yieldValue(`Child:${shouldSuspend}/${text}`);
718+
return null;
719+
}
720+
721+
const root = ReactDOMClient.createRoot(container);
722+
await act(async () => {
723+
root.render(<App />);
724+
});
725+
expect(Scheduler).toHaveYielded(['Child:false/0']);
726+
727+
await act(() => {
728+
window.dispatchEvent(new Event('popstate'));
729+
});
730+
expect(Scheduler).toHaveYielded(['popState', 'Suspend!']);
731+
732+
await act(async () => {
733+
_setText(true);
734+
});
735+
736+
// Transition lane yields
737+
expect(Scheduler).toHaveYielded(['Child:false/true', 'Suspend!']);
738+
739+
root.unmount();
740+
});
661741
});

packages/react-native-renderer/src/ReactFabricHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,10 @@ export function getCurrentEventPriority(): * {
506506
return DefaultEventPriority;
507507
}
508508

509+
export function getIsCurrentEventPopState(): boolean {
510+
return false;
511+
}
512+
509513
// The Fabric renderer is secondary to the existing React Native renderer.
510514
export const isPrimaryRenderer = false;
511515

packages/react-native-renderer/src/ReactNativeHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ export function getCurrentEventPriority(): * {
260260
return DefaultEventPriority;
261261
}
262262

263+
export function getIsCurrentEventPopState(): boolean {
264+
return false;
265+
}
266+
263267
// -------------------
264268
// Mutation
265269
// -------------------

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
442442
return currentEventPriority;
443443
},
444444

445+
getIsCurrentEventPopState(): boolean {
446+
return false;
447+
},
448+
445449
now: Scheduler.unstable_now,
446450

447451
isPrimaryRenderer: true,

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
223223
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
224224
}
225225
}
226+
// Batch transitions in `popstate` with the sync lane
227+
if (root.forceSync && isTransitionLane(nonIdleUnblockedLanes)) {
228+
nextLanes |= nonIdleUnblockedLanes & TransitionLanes;
229+
}
226230
} else {
227231
// The only remaining work is Idle.
228232
const unblockedLanes = pendingLanes & ~suspendedLanes;
@@ -315,7 +319,6 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
315319
lanes &= ~lane;
316320
}
317321
}
318-
319322
return nextLanes;
320323
}
321324

@@ -471,8 +474,11 @@ export function getLanesToRetrySynchronouslyOnError(
471474
return NoLanes;
472475
}
473476

474-
export function includesSyncLane(lanes: Lanes): boolean {
475-
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
477+
export function includesSyncLaneOrForceSync(
478+
lanes: Lanes,
479+
root: FiberRoot,
480+
): boolean {
481+
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes || root.forceSync;
476482
}
477483

478484
export function includesNonIdleWork(lanes: Lanes): boolean {
@@ -633,6 +639,7 @@ export function markRootUpdated(
633639
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
634640
root.suspendedLanes |= suspendedLanes;
635641
root.pingedLanes &= ~suspendedLanes;
642+
root.forceSync = false;
636643

637644
// The suspended lanes are no longer CPU-bound. Clear their expiration times.
638645
const expirationTimes = root.expirationTimes;
@@ -671,6 +678,8 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
671678

672679
root.errorRecoveryDisabledLanes &= remainingLanes;
673680

681+
root.forceSync = false;
682+
674683
const entanglements = root.entanglements;
675684
const eventTimes = root.eventTimes;
676685
const expirationTimes = root.expirationTimes;

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
scheduleMicrotask,
8585
prepareRendererToRender,
8686
resetRendererAfterRender,
87+
getIsCurrentEventPopState,
8788
} from './ReactFiberHostConfig';
8889

8990
import {
@@ -137,7 +138,7 @@ import {
137138
NoTimestamp,
138139
claimNextTransitionLane,
139140
claimNextRetryLane,
140-
includesSyncLane,
141+
includesSyncLaneOrForceSync,
141142
isSubsetOfLanes,
142143
mergeLanes,
143144
removeLanes,
@@ -147,6 +148,7 @@ import {
147148
includesOnlyTransitions,
148149
includesBlockingLane,
149150
includesExpiredLane,
151+
isTransitionLane,
150152
getNextLanes,
151153
markStarvedLanesAsExpired,
152154
getLanesToRetrySynchronouslyOnError,
@@ -727,6 +729,7 @@ export function scheduleUpdateOnFiber(
727729
}
728730

729731
// Mark that the root has a pending update.
732+
markRootWithPopState(root, lane);
730733
markRootUpdated(root, lane, eventTime);
731734

732735
if (
@@ -914,7 +917,7 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
914917
// TODO: Temporary until we confirm this warning is not fired.
915918
if (
916919
existingCallbackNode == null &&
917-
!includesSyncLane(existingCallbackPriority)
920+
!includesSyncLaneOrForceSync(existingCallbackPriority, root)
918921
) {
919922
console.error(
920923
'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
@@ -932,7 +935,7 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
932935

933936
// Schedule a new callback.
934937
let newCallbackNode;
935-
if (includesSyncLane(newCallbackPriority)) {
938+
if (includesSyncLaneOrForceSync(newCallbackPriority, root)) {
936939
// Special case: Sync React callbacks are scheduled on a special
937940
// internal queue
938941
if (root.tag === LegacyRoot) {
@@ -1455,6 +1458,12 @@ function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
14551458
markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes);
14561459
}
14571460

1461+
function markRootWithPopState(root: FiberRoot, updateLane: Lane) {
1462+
if (isTransitionLane(updateLane) && getIsCurrentEventPopState()) {
1463+
root.forceSync = true;
1464+
}
1465+
}
1466+
14581467
// This is the entry point for synchronous tasks that don't go
14591468
// through Scheduler
14601469
function performSyncWorkOnRoot(root: FiberRoot) {
@@ -1469,7 +1478,7 @@ function performSyncWorkOnRoot(root: FiberRoot) {
14691478
flushPassiveEffects();
14701479

14711480
let lanes = getNextLanes(root, NoLanes);
1472-
if (!includesSyncLane(lanes)) {
1481+
if (!includesSyncLaneOrForceSync(lanes, root)) {
14731482
// There's no remaining sync work left.
14741483
ensureRootIsScheduled(root, now());
14751484
return null;
@@ -2920,13 +2929,16 @@ function commitRootImpl(
29202929
// TODO: We can optimize this by not scheduling the callback earlier. Since we
29212930
// currently schedule the callback in multiple places, will wait until those
29222931
// are consolidated.
2923-
if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) {
2932+
if (
2933+
includesSyncLaneOrForceSync(pendingPassiveEffectsLanes, root) &&
2934+
root.tag !== LegacyRoot
2935+
) {
29242936
flushPassiveEffects();
29252937
}
29262938

29272939
// Read this again, since a passive effect might have updated it
29282940
remainingLanes = root.pendingLanes;
2929-
if (includesSyncLane(remainingLanes)) {
2941+
if (includesSyncLaneOrForceSync(remainingLanes, root)) {
29302942
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
29312943
markNestedUpdateScheduled();
29322944
}

packages/react-reconciler/src/ReactInternalTypes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ type BaseFiberRootProperties = {
259259
pooledCache: Cache | null,
260260
pooledCacheLanes: Lanes,
261261

262+
forceSync: boolean,
263+
262264
// TODO: In Fizz, id generation is specific to each server config. Maybe we
263265
// should do this in Fiber, too? Deferring this decision for now because
264266
// there's no other place to store the prefix except for an internal field on

packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ describe('ReactFiberHostContext', () => {
7070
getCurrentEventPriority: function() {
7171
return DefaultEventPriority;
7272
},
73+
getIsCurrentEventPopState() {
74+
return false;
75+
},
7376
requestPostPaintCallback: function() {},
7477
prepareRendererToRender: function() {},
7578
resetRendererAfterRender: function() {},

0 commit comments

Comments
 (0)