diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js
index 591072cec84..bcf52149fd6 100644
--- a/packages/react-art/src/ReactFiberConfigART.js
+++ b/packages/react-art/src/ReactFiberConfigART.js
@@ -346,6 +346,10 @@ export function getCurrentEventPriority() {
return DefaultEventPriority;
}
+export function shouldAttemptEagerTransition() {
+ return false;
+}
+
// The ART renderer is secondary to the React DOM renderer.
export const isPrimaryRenderer = false;
diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 93c8950b055..2315c005479 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -527,6 +527,10 @@ export function getCurrentEventPriority(): EventPriority {
return getEventPriority(currentEvent.type);
}
+export function shouldAttemptEagerTransition(): boolean {
+ return window.event && window.event.type === 'popstate';
+}
+
export const isPrimaryRenderer = true;
export const warnsIfNotActing = true;
// This initialization code may run even on server environments
diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
index 61bb7a318d6..79bf3416053 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
@@ -27,7 +27,6 @@ describe('ReactDOMFiberAsync', () => {
let container;
beforeEach(() => {
- jest.resetModules();
container = document.createElement('div');
React = require('react');
ReactDOM = require('react-dom');
@@ -40,6 +39,7 @@ describe('ReactDOMFiberAsync', () => {
assertLog = InternalTestUtils.assertLog;
document.body.appendChild(container);
+ window.event = undefined;
});
afterEach(() => {
@@ -566,4 +566,139 @@ describe('ReactDOMFiberAsync', () => {
expect(container.textContent).toBe('new');
});
+
+ it('should synchronously render the transition lane scheduled in a popState', async () => {
+ function App() {
+ const [syncState, setSyncState] = React.useState(false);
+ const [hasNavigated, setHasNavigated] = React.useState(false);
+ function onPopstate() {
+ Scheduler.log(`popState`);
+ React.startTransition(() => {
+ setHasNavigated(true);
+ });
+ setSyncState(true);
+ }
+ React.useEffect(() => {
+ window.addEventListener('popstate', onPopstate);
+ return () => {
+ window.removeEventListener('popstate', onPopstate);
+ };
+ }, []);
+ Scheduler.log(`render:${hasNavigated}/${syncState}`);
+ return null;
+ }
+ const root = ReactDOMClient.createRoot(container);
+ await act(async () => {
+ root.render();
+ });
+ assertLog(['render:false/false']);
+
+ await act(async () => {
+ const popStateEvent = new Event('popstate');
+ // Jest is not emulating window.event correctly in the microtask
+ window.event = popStateEvent;
+ window.dispatchEvent(popStateEvent);
+ queueMicrotask(() => {
+ window.event = undefined;
+ });
+ });
+
+ assertLog(['popState', 'render:true/true']);
+ await act(() => {
+ root.unmount();
+ });
+ });
+
+ it('Should not flush transition lanes if there is no transition scheduled in popState', async () => {
+ let setHasNavigated;
+ function App() {
+ const [syncState, setSyncState] = React.useState(false);
+ const [hasNavigated, _setHasNavigated] = React.useState(false);
+ setHasNavigated = _setHasNavigated;
+ function onPopstate() {
+ setSyncState(true);
+ }
+
+ React.useEffect(() => {
+ window.addEventListener('popstate', onPopstate);
+ return () => {
+ window.removeEventListener('popstate', onPopstate);
+ };
+ }, []);
+
+ Scheduler.log(`render:${hasNavigated}/${syncState}`);
+ return null;
+ }
+ const root = ReactDOMClient.createRoot(container);
+ await act(async () => {
+ root.render();
+ });
+ assertLog(['render:false/false']);
+
+ React.startTransition(() => {
+ setHasNavigated(true);
+ });
+ await act(async () => {
+ const popStateEvent = new Event('popstate');
+ // Jest is not emulating window.event correctly in the microtask
+ window.event = popStateEvent;
+ window.dispatchEvent(popStateEvent);
+ queueMicrotask(() => {
+ window.event = undefined;
+ });
+ });
+ assertLog(['render:false/true', 'render:true/true']);
+ await act(() => {
+ root.unmount();
+ });
+ });
+
+ it('transition lane in popState should yield if it suspends', async () => {
+ const never = {then() {}};
+ let _setText;
+
+ function App() {
+ const [shouldSuspend, setShouldSuspend] = React.useState(false);
+ const [text, setText] = React.useState('0');
+ _setText = setText;
+ if (shouldSuspend) {
+ Scheduler.log('Suspend!');
+ throw never;
+ }
+ function onPopstate() {
+ React.startTransition(() => {
+ setShouldSuspend(val => !val);
+ });
+ }
+ React.useEffect(() => {
+ window.addEventListener('popstate', onPopstate);
+ return () => window.removeEventListener('popstate', onPopstate);
+ }, []);
+ Scheduler.log(`Child:${shouldSuspend}/${text}`);
+ return text;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(async () => {
+ root.render();
+ });
+ assertLog(['Child:false/0']);
+
+ await act(() => {
+ const popStateEvent = new Event('popstate');
+ window.event = popStateEvent;
+ window.dispatchEvent(popStateEvent);
+ queueMicrotask(() => {
+ window.event = undefined;
+ });
+ });
+ assertLog(['Suspend!']);
+
+ await act(async () => {
+ _setText('1');
+ });
+ assertLog(['Child:false/1', 'Suspend!']);
+
+ root.unmount();
+ });
});
diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js
index 5dd36c20baa..28bdfcb55dc 100644
--- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js
+++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js
@@ -334,6 +334,10 @@ export function getCurrentEventPriority(): * {
return DefaultEventPriority;
}
+export function shouldAttemptEagerTransition(): boolean {
+ return false;
+}
+
// The Fabric renderer is secondary to the existing React Native renderer.
export const isPrimaryRenderer = false;
diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js
index b218a1f77c2..5467ecf72d3 100644
--- a/packages/react-native-renderer/src/ReactFiberConfigNative.js
+++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js
@@ -265,6 +265,10 @@ export function getCurrentEventPriority(): * {
return DefaultEventPriority;
}
+export function shouldAttemptEagerTransition(): boolean {
+ return false;
+}
+
// -------------------
// Mutation
// -------------------
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index f32ec38806b..fe2e382f353 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -526,6 +526,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return currentEventPriority;
},
+ shouldAttemptEagerTransition(): boolean {
+ return false;
+ },
+
now: Scheduler.unstable_now,
isPrimaryRenderer: true,
diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js
index 72065797458..86f882a0257 100644
--- a/packages/react-reconciler/src/ReactFiberRootScheduler.js
+++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js
@@ -20,6 +20,8 @@ import {
getNextLanes,
includesSyncLane,
markStarvedLanesAsExpired,
+ markRootEntangled,
+ mergeLanes,
} from './ReactFiberLane';
import {
CommitContext,
@@ -49,7 +51,11 @@ import {
IdleEventPriority,
lanesToEventPriority,
} from './ReactEventPriorities';
-import {supportsMicrotasks, scheduleMicrotask} from './ReactFiberConfig';
+import {
+ supportsMicrotasks,
+ scheduleMicrotask,
+ shouldAttemptEagerTransition,
+} from './ReactFiberConfig';
import ReactSharedInternals from 'shared/ReactSharedInternals';
const {ReactCurrentActQueue} = ReactSharedInternals;
@@ -72,6 +78,8 @@ let mightHavePendingSyncWork: boolean = false;
let isFlushingWork: boolean = false;
+let currentEventTransitionLane: Lane = NoLanes;
+
export function ensureRootIsScheduled(root: FiberRoot): void {
// This function is called whenever a root receives an update. It does two
// things 1) it ensures the root is in the root schedule, and 2) it ensures
@@ -238,6 +246,14 @@ function processRootScheduleInMicrotask() {
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
+
+ if (
+ currentEventTransitionLane !== NoLane &&
+ shouldAttemptEagerTransition()
+ ) {
+ markRootEntangled(root, mergeLanes(currentEventTransitionLane, SyncLane));
+ }
+
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
if (nextLanes === NoLane) {
// This root has no more pending work. Remove it from the schedule. To
@@ -267,6 +283,8 @@ function processRootScheduleInMicrotask() {
root = next;
}
+ currentEventTransitionLane = NoLane;
+
// At the end of the microtask, flush any pending synchronous work. This has
// to come at the end, because it does actual rendering work that might throw.
flushSyncWorkOnAllRoots();
@@ -472,3 +490,11 @@ function scheduleImmediateTask(cb: () => mixed) {
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
}
}
+
+export function getCurrentEventTransitionLane(): Lane {
+ return currentEventTransitionLane;
+}
+
+export function setCurrentEventTransitionLane(lane: Lane): void {
+ currentEventTransitionLane = lane;
+}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 6f00533d300..a25cf0934a7 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -279,6 +279,8 @@ import {
flushSyncWorkOnAllRoots,
flushSyncWorkOnLegacyRootsOnly,
getContinuationForRoot,
+ getCurrentEventTransitionLane,
+ setCurrentEventTransitionLane,
} from './ReactFiberRootScheduler';
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
@@ -583,8 +585,6 @@ const NESTED_PASSIVE_UPDATE_LIMIT = 50;
let nestedPassiveUpdateCount: number = 0;
let rootWithPassiveNestedUpdates: FiberRoot | null = null;
-let currentEventTransitionLane: Lanes = NoLanes;
-
let isRunningInsertionEffect = false;
export function getWorkInProgressRoot(): FiberRoot | null {
@@ -641,11 +641,11 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// The trick we use is to cache the first of each of these inputs within an
// event. Then reset the cached values once we can be sure the event is
// over. Our heuristic for that is whenever we enter a concurrent work loop.
- if (currentEventTransitionLane === NoLane) {
+ if (getCurrentEventTransitionLane() === NoLane) {
// All transitions within the same event are assigned the same lane.
- currentEventTransitionLane = claimNextTransitionLane();
+ setCurrentEventTransitionLane(claimNextTransitionLane());
}
- return currentEventTransitionLane;
+ return getCurrentEventTransitionLane();
}
// Updates originating inside certain React methods, like flushSync, have
@@ -849,8 +849,6 @@ export function performConcurrentWorkOnRoot(
resetNestedUpdateFlag();
}
- currentEventTransitionLane = NoLanes;
-
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
index a4c32090bfd..bfbcddfcb73 100644
--- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
@@ -70,6 +70,9 @@ describe('ReactFiberHostContext', () => {
getCurrentEventPriority: function () {
return DefaultEventPriority;
},
+ shouldAttemptEagerTransition() {
+ return false;
+ },
requestPostPaintCallback: function () {},
maySuspendCommit(type, props) {
return false;
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
index e791f63d7ec..9e579dca47e 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
@@ -66,6 +66,8 @@ export const preparePortalMount = $$$config.preparePortalMount;
export const prepareScopeUpdate = $$$config.prepareScopeUpdate;
export const getInstanceFromScope = $$$config.getInstanceFromScope;
export const getCurrentEventPriority = $$$config.getCurrentEventPriority;
+export const shouldAttemptEagerTransition =
+ $$$config.shouldAttemptEagerTransition;
export const detachDeletedInstance = $$$config.detachDeletedInstance;
export const requestPostPaintCallback = $$$config.requestPostPaintCallback;
export const maySuspendCommit = $$$config.maySuspendCommit;
diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
index 1b7e9adcee8..3b478b01276 100644
--- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
@@ -213,6 +213,9 @@ export function createTextInstance(
export function getCurrentEventPriority(): * {
return DefaultEventPriority;
}
+export function shouldAttemptEagerTransition(): boolean {
+ return false;
+}
export const isPrimaryRenderer = false;
export const warnsIfNotActing = true;