Skip to content

Commit fec97ec

Browse files
authored
act: Move didScheduleLegacyUpdate to ensureRootIsScheduled (#26552)
`act` uses the `didScheduleLegacyUpdate` field to simulate the behavior of batching in React <17 and below. It's a quirk leftover from a previous implementation, not intentionally designed. This sets `didScheduleLegacyUpdate` every time a legacy root receives an update as opposed to only when the `executionContext` is empty. There's no real reason to do it this way over some other way except that it's how it used to work before #26512 and we should try our best to maintain the existing behavior, quirks and all, since existing tests may have come to accidentally rely on it. This should fix some (though not all) of the internal Meta tests that started failing after #26512 landed. Will add a regression test before merging.
1 parent 9a9da77 commit fec97ec

4 files changed

Lines changed: 80 additions & 15 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
119119
// unblock additional features we have planned.
120120
scheduleTaskForRootDuringMicrotask(root,now());
121121
}
122+
123+
if(
124+
__DEV__&&
125+
ReactCurrentActQueue.isBatchingLegacy&&
126+
root.tag===LegacyRoot
127+
){
128+
// Special `act` case: Record whenever a legacy update is scheduled.
129+
ReactCurrentActQueue.didScheduleLegacyUpdate=true;
130+
}
122131
}
123132

124133
exportfunctionflushSyncWorkOnAllRoots(){

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,6 @@ export function scheduleUpdateOnFiber(
828828
){
829829
if(__DEV__&&ReactCurrentActQueue.isBatchingLegacy){
830830
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
831-
ReactCurrentActQueue.didScheduleLegacyUpdate=true;
832831
}else{
833832
// Flush the synchronous work now, unless we're already working or inside
834833
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of

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

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ let Suspense;
1717
letDiscreteEventPriority;
1818
letstartTransition;
1919
letwaitForMicrotasks;
20+
letScheduler;
21+
letassertLog;
2022

2123
describe('isomorphic act()',()=>{
2224
beforeEach(()=>{
2325
React=require('react');
26+
Scheduler=require('scheduler');
2427

2528
ReactNoop=require('react-noop-renderer');
2629
DiscreteEventPriority=
@@ -31,6 +34,7 @@ describe('isomorphic act()', () => {
3134
startTransition=React.startTransition;
3235

3336
waitForMicrotasks=require('internal-test-utils').waitForMicrotasks;
37+
assertLog=require('internal-test-utils').assertLog;
3438
});
3539

3640
beforeEach(()=>{
@@ -41,6 +45,11 @@ describe('isomorphic act()', () => {
4145
jest.restoreAllMocks();
4246
});
4347

48+
functionText({text}){
49+
Scheduler.log(text);
50+
returntext;
51+
}
52+
4453
// @gate __DEV__
4554
test('bypasses queueMicrotask',async()=>{
4655
constroot=ReactNoop.createRoot();
@@ -132,19 +141,67 @@ describe('isomorphic act()', () => {
132141
constroot=ReactNoop.createLegacyRoot();
133142

134143
awaitact(async()=>{
135-
// These updates are batched. This replicates the behavior of the original
136-
// `act` implementation, for compatibility.
137-
root.render('A');
138-
root.render('B');
139-
// Nothing has rendered yet.
140-
expect(root).toMatchRenderedOutput(null);
144+
queueMicrotask(()=>{
145+
Scheduler.log('Current tree in microtask: '+root.getChildrenAsJSX());
146+
root.render(<Texttext="C"/>);
147+
});
148+
root.render(<Texttext="A"/>);
149+
root.render(<Texttext="B"/>);
150+
141151
awaitnull;
142-
// Updates are flushed after the first await.
143-
expect(root).toMatchRenderedOutput('B');
152+
assertLog([
153+
// A and B should render in a single batch _before_ the microtask queue
154+
// has run. This replicates the behavior of the original `act`
155+
// implementation, for compatibility.
156+
'B',
157+
'Current tree in microtask: B',
158+
159+
// C isn't scheduled until a microtask, so it's rendered separately.
160+
'C',
161+
]);
162+
163+
// Subsequent updates should also render in separate batches.
164+
root.render(<Texttext="D"/>);
165+
root.render(<Texttext="E"/>);
166+
assertLog(['D','E']);
167+
});
168+
});
144169

145-
// Subsequent updates in the same scope aren't batched.
146-
root.render('C');
147-
expect(root).toMatchRenderedOutput('C');
170+
// @gate __DEV__
171+
test('in legacy mode, in an async scope, updates are batched until the first `await` (regression test: batchedUpdates)',async()=>{
172+
constroot=ReactNoop.createLegacyRoot();
173+
174+
awaitact(async()=>{
175+
queueMicrotask(()=>{
176+
Scheduler.log('Current tree in microtask: '+root.getChildrenAsJSX());
177+
root.render(<Texttext="C"/>);
178+
});
179+
180+
// This is a regression test. The presence of `batchedUpdates` would cause
181+
// these updates to not flush until a microtask. The correct behavior is
182+
// that they flush before the microtask queue, regardless of whether
183+
// they are wrapped with `batchedUpdates`.
184+
ReactNoop.batchedUpdates(()=>{
185+
root.render(<Texttext="A"/>);
186+
root.render(<Texttext="B"/>);
187+
});
188+
189+
awaitnull;
190+
assertLog([
191+
// A and B should render in a single batch _before_ the microtask queue
192+
// has run. This replicates the behavior of the original `act`
193+
// implementation, for compatibility.
194+
'B',
195+
'Current tree in microtask: B',
196+
197+
// C isn't scheduled until a microtask, so it's rendered separately.
198+
'C',
199+
]);
200+
201+
// Subsequent updates should also render in separate batches.
202+
root.render(<Texttext="D"/>);
203+
root.render(<Texttext="E"/>);
204+
assertLog(['D','E']);
148205
});
149206
});
150207

‎scripts/jest/matchers/reactTestMatchers.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,21 @@ function captureAssertion(fn) {
2020
return{pass: true};
2121
}
2222

23-
functionassertYieldsWereCleared(Scheduler){
23+
functionassertYieldsWereCleared(Scheduler,caller){
2424
constactualYields=Scheduler.unstable_clearLog();
2525
if(actualYields.length!==0){
2626
consterror=Error(
2727
'The event log is not empty. Call assertLog(...) first.'
2828
);
29-
Error.captureStackTrace(error,assertYieldsWereCleared);
29+
Error.captureStackTrace(error,caller);
3030
throwerror;
3131
}
3232
}
3333

3434
functiontoMatchRenderedOutput(ReactNoop,expectedJSX){
3535
if(typeofReactNoop.getChildrenAsJSX==='function'){
3636
constScheduler=ReactNoop._Scheduler;
37-
assertYieldsWereCleared(Scheduler);
37+
assertYieldsWereCleared(Scheduler,toMatchRenderedOutput);
3838
returncaptureAssertion(()=>{
3939
expect(ReactNoop.getChildrenAsJSX()).toEqual(expectedJSX);
4040
});

0 commit comments

Comments
 (0)