Skip to content

act: Move didScheduleLegacyUpdate to ensureRootIsScheduled #26552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
// unblock additional features we have planned.
scheduleTaskForRootDuringMicrotask(root, now());
}

if (
__DEV__ &&
ReactCurrentActQueue.isBatchingLegacy &&
root.tag === LegacyRoot
) {
// Special `act` case: Record whenever a legacy update is scheduled.
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
}
}

export function flushSyncWorkOnAllRoots() {
Expand Down
1 change: 0 additions & 1 deletion packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,6 @@ export function scheduleUpdateOnFiber(
) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy) {
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
} else {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
Expand Down
79 changes: 68 additions & 11 deletions packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ let Suspense;
let DiscreteEventPriority;
let startTransition;
let waitForMicrotasks;
let Scheduler;
let assertLog;

describe('isomorphic act()', () => {
beforeEach(() => {
React = require('react');
Scheduler = require('scheduler');

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

waitForMicrotasks = require('internal-test-utils').waitForMicrotasks;
assertLog = require('internal-test-utils').assertLog;
});

beforeEach(() => {
Expand All @@ -41,6 +45,11 @@ describe('isomorphic act()', () => {
jest.restoreAllMocks();
});

function Text({text}) {
Scheduler.log(text);
return text;
}

// @gate __DEV__
test('bypasses queueMicrotask', async () => {
const root = ReactNoop.createRoot();
Expand Down Expand Up @@ -132,19 +141,67 @@ describe('isomorphic act()', () => {
const root = ReactNoop.createLegacyRoot();

await act(async () => {
// These updates are batched. This replicates the behavior of the original
// `act` implementation, for compatibility.
root.render('A');
root.render('B');
// Nothing has rendered yet.
expect(root).toMatchRenderedOutput(null);
queueMicrotask(() => {
Scheduler.log('Current tree in microtask: ' + root.getChildrenAsJSX());
root.render(<Text text="C" />);
});
root.render(<Text text="A" />);
root.render(<Text text="B" />);

await null;
// Updates are flushed after the first await.
expect(root).toMatchRenderedOutput('B');
assertLog([
// A and B should render in a single batch _before_ the microtask queue
// has run. This replicates the behavior of the original `act`
// implementation, for compatibility.
'B',
'Current tree in microtask: B',

// C isn't scheduled until a microtask, so it's rendered separately.
'C',
]);

// Subsequent updates should also render in separate batches.
root.render(<Text text="D" />);
root.render(<Text text="E" />);
assertLog(['D', 'E']);
});
});

// Subsequent updates in the same scope aren't batched.
root.render('C');
expect(root).toMatchRenderedOutput('C');
// @gate __DEV__
test('in legacy mode, in an async scope, updates are batched until the first `await` (regression test: batchedUpdates)', async () => {
const root = ReactNoop.createLegacyRoot();

await act(async () => {
queueMicrotask(() => {
Scheduler.log('Current tree in microtask: ' + root.getChildrenAsJSX());
root.render(<Text text="C" />);
});

// This is a regression test. The presence of `batchedUpdates` would cause
// these updates to not flush until a microtask. The correct behavior is
// that they flush before the microtask queue, regardless of whether
// they are wrapped with `batchedUpdates`.
ReactNoop.batchedUpdates(() => {
root.render(<Text text="A" />);
root.render(<Text text="B" />);
});

await null;
assertLog([
// A and B should render in a single batch _before_ the microtask queue
// has run. This replicates the behavior of the original `act`
// implementation, for compatibility.
'B',
'Current tree in microtask: B',

// C isn't scheduled until a microtask, so it's rendered separately.
'C',
]);

// Subsequent updates should also render in separate batches.
root.render(<Text text="D" />);
root.render(<Text text="E" />);
assertLog(['D', 'E']);
});
});

Expand Down
6 changes: 3 additions & 3 deletions scripts/jest/matchers/reactTestMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@ function captureAssertion(fn) {
return {pass: true};
}

function assertYieldsWereCleared(Scheduler) {
function assertYieldsWereCleared(Scheduler, caller) {
const actualYields = Scheduler.unstable_clearLog();
if (actualYields.length !== 0) {
const error = Error(
'The event log is not empty. Call assertLog(...) first.'
);
Error.captureStackTrace(error, assertYieldsWereCleared);
Error.captureStackTrace(error, caller);
throw error;
}
}

function toMatchRenderedOutput(ReactNoop, expectedJSX) {
if (typeof ReactNoop.getChildrenAsJSX === 'function') {
const Scheduler = ReactNoop._Scheduler;
assertYieldsWereCleared(Scheduler);
assertYieldsWereCleared(Scheduler, toMatchRenderedOutput);
return captureAssertion(() => {
expect(ReactNoop.getChildrenAsJSX()).toEqual(expectedJSX);
});
Expand Down