Skip to content

Commit 32eefcb

Browse files
authored
Replace flushDiscreteUpdates with flushSync (facebook#21775)
* Replace flushDiscreteUpdates with flushSync flushDiscreteUpdates is almost the same as flushSync. It forces passive effects to fire, because of an outdated heuristic, which isn't ideal but not that important. Besides that, the only remaining difference between flushDiscreteUpdates and flushSync is that flushDiscreteUpdates does not warn if you call it from inside an effect/lifecycle. This is because it might get triggered by a nested event dispatch, like `el.focus()`. So I added a new method, flushSyncWithWarningIfAlreadyRendering, which is used for the public flushSync API. It includes the warning. And I removed the warning from flushSync, so the event system can call that one. In production, flushSyncWithWarningIfAlreadyRendering gets inlined to flushSync, so the behavior is identical. Another way of thinking about this PR is that I renamed flushSync to flushSyncWithWarningIfAlreadyRendering and flushDiscreteUpdates to flushSync (and fixed the passive effects thing). The point is to prevent these from subtly diverging in the future. * Invert so the one with the warning is the default one To make Seb happy
1 parent c5cfa71 commit 32eefcb

11 files changed

+73
-99
lines changed

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

+1-7
Original file line numberDiff line numberDiff line change
@@ -1154,19 +1154,13 @@ describe('ReactDOMFiber', () => {
11541154
expect(ops).toEqual(['A']);
11551155

11561156
if (__DEV__) {
1157-
const errorCalls = console.error.calls.count();
1157+
expect(console.error.calls.count()).toBe(2);
11581158
expect(console.error.calls.argsFor(0)[0]).toMatch(
11591159
'ReactDOM.render is no longer supported in React 18',
11601160
);
11611161
expect(console.error.calls.argsFor(1)[0]).toMatch(
11621162
'ReactDOM.render is no longer supported in React 18',
11631163
);
1164-
// TODO: this warning shouldn't be firing in the first place if user didn't call it.
1165-
for (let i = 2; i < errorCalls; i++) {
1166-
expect(console.error.calls.argsFor(i)[0]).toMatch(
1167-
'unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.',
1168-
);
1169-
}
11701164
}
11711165
});
11721166

packages/react-dom/src/client/ReactDOM.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {createEventHandle} from './ReactDOMEventHandle';
2323
import {
2424
batchedUpdates,
2525
discreteUpdates,
26-
flushDiscreteUpdates,
2726
flushSync,
27+
flushSyncWithoutWarningIfAlreadyRendering,
2828
flushControlled,
2929
injectIntoDevTools,
3030
attemptSynchronousHydration,
@@ -100,7 +100,7 @@ setRestoreImplementation(restoreControlledState);
100100
setBatchingImplementation(
101101
batchedUpdates,
102102
discreteUpdates,
103-
flushDiscreteUpdates,
103+
flushSyncWithoutWarningIfAlreadyRendering,
104104
);
105105

106106
function createPortal(

packages/react-dom/src/events/ReactDOMUpdateBatching.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ let batchedUpdatesImpl = function(fn, bookkeeping) {
2323
let discreteUpdatesImpl = function(fn, a, b, c, d) {
2424
return fn(a, b, c, d);
2525
};
26-
let flushDiscreteUpdatesImpl = function() {};
26+
let flushSyncImpl = function() {};
2727

2828
let isInsideEventHandler = false;
2929

@@ -39,7 +39,7 @@ function finishEventHandler() {
3939
// bails out of the update without touching the DOM.
4040
// TODO: Restore state in the microtask, after the discrete updates flush,
4141
// instead of early flushing them here.
42-
flushDiscreteUpdatesImpl();
42+
flushSyncImpl();
4343
restoreStateIfNeeded();
4444
}
4545
}
@@ -67,9 +67,9 @@ export function discreteUpdates(fn, a, b, c, d) {
6767
export function setBatchingImplementation(
6868
_batchedUpdatesImpl,
6969
_discreteUpdatesImpl,
70-
_flushDiscreteUpdatesImpl,
70+
_flushSyncImpl,
7171
) {
7272
batchedUpdatesImpl = _batchedUpdatesImpl;
7373
discreteUpdatesImpl = _discreteUpdatesImpl;
74-
flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
74+
flushSyncImpl = _flushSyncImpl;
7575
}

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

-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export const {
4141
unbatchedUpdates,
4242
discreteUpdates,
4343
idleUpdates,
44-
flushDiscreteUpdates,
4544
flushSync,
4645
flushPassiveEffects,
4746
act,

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

-2
Original file line numberDiff line numberDiff line change
@@ -915,8 +915,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
915915
}
916916
},
917917

918-
flushDiscreteUpdates: NoopRenderer.flushDiscreteUpdates,
919-
920918
flushSync(fn: () => mixed) {
921919
NoopRenderer.flushSync(fn);
922920
},

packages/react-reconciler/src/ReactFiberReconciler.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {
2121
unbatchedUpdates as unbatchedUpdates_old,
2222
deferredUpdates as deferredUpdates_old,
2323
discreteUpdates as discreteUpdates_old,
24-
flushDiscreteUpdates as flushDiscreteUpdates_old,
2524
flushControlled as flushControlled_old,
2625
flushSync as flushSync_old,
26+
flushSyncWithoutWarningIfAlreadyRendering as flushSyncWithoutWarningIfAlreadyRendering_old,
2727
flushPassiveEffects as flushPassiveEffects_old,
2828
getPublicRootInstance as getPublicRootInstance_old,
2929
attemptSynchronousHydration as attemptSynchronousHydration_old,
@@ -59,9 +59,9 @@ import {
5959
unbatchedUpdates as unbatchedUpdates_new,
6060
deferredUpdates as deferredUpdates_new,
6161
discreteUpdates as discreteUpdates_new,
62-
flushDiscreteUpdates as flushDiscreteUpdates_new,
6362
flushControlled as flushControlled_new,
6463
flushSync as flushSync_new,
64+
flushSyncWithoutWarningIfAlreadyRendering as flushSyncWithoutWarningIfAlreadyRendering_new,
6565
flushPassiveEffects as flushPassiveEffects_new,
6666
getPublicRootInstance as getPublicRootInstance_new,
6767
attemptSynchronousHydration as attemptSynchronousHydration_new,
@@ -108,13 +108,13 @@ export const deferredUpdates = enableNewReconciler
108108
export const discreteUpdates = enableNewReconciler
109109
? discreteUpdates_new
110110
: discreteUpdates_old;
111-
export const flushDiscreteUpdates = enableNewReconciler
112-
? flushDiscreteUpdates_new
113-
: flushDiscreteUpdates_old;
114111
export const flushControlled = enableNewReconciler
115112
? flushControlled_new
116113
: flushControlled_old;
117114
export const flushSync = enableNewReconciler ? flushSync_new : flushSync_old;
115+
export const flushSyncWithoutWarningIfAlreadyRendering = enableNewReconciler
116+
? flushSyncWithoutWarningIfAlreadyRendering_new
117+
: flushSyncWithoutWarningIfAlreadyRendering_old;
118118
export const flushPassiveEffects = enableNewReconciler
119119
? flushPassiveEffects_new
120120
: flushPassiveEffects_old;

packages/react-reconciler/src/ReactFiberReconciler.new.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import {
5757
flushControlled,
5858
deferredUpdates,
5959
discreteUpdates,
60-
flushDiscreteUpdates,
60+
flushSyncWithoutWarningIfAlreadyRendering,
6161
flushPassiveEffects,
6262
} from './ReactFiberWorkLoop.new';
6363
import {
@@ -330,9 +330,9 @@ export {
330330
unbatchedUpdates,
331331
deferredUpdates,
332332
discreteUpdates,
333-
flushDiscreteUpdates,
334333
flushControlled,
335334
flushSync,
335+
flushSyncWithoutWarningIfAlreadyRendering,
336336
flushPassiveEffects,
337337
};
338338

packages/react-reconciler/src/ReactFiberReconciler.old.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import {
5757
flushControlled,
5858
deferredUpdates,
5959
discreteUpdates,
60-
flushDiscreteUpdates,
60+
flushSyncWithoutWarningIfAlreadyRendering,
6161
flushPassiveEffects,
6262
} from './ReactFiberWorkLoop.old';
6363
import {
@@ -330,9 +330,9 @@ export {
330330
unbatchedUpdates,
331331
deferredUpdates,
332332
discreteUpdates,
333-
flushDiscreteUpdates,
334333
flushControlled,
335334
flushSync,
335+
flushSyncWithoutWarningIfAlreadyRendering,
336336
flushPassiveEffects,
337337
};
338338

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

+17-37
Original file line numberDiff line numberDiff line change
@@ -1044,34 +1044,6 @@ export function getExecutionContext(): ExecutionContext {
10441044
return executionContext;
10451045
}
10461046

1047-
export function flushDiscreteUpdates() {
1048-
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
1049-
// However, `act` uses `batchedUpdates`, so there's no way to distinguish
1050-
// those two cases. Need to fix this before exposing flushDiscreteUpdates
1051-
// as a public API.
1052-
if (
1053-
(executionContext & (BatchedContext | RenderContext | CommitContext)) !==
1054-
NoContext
1055-
) {
1056-
if (__DEV__) {
1057-
if ((executionContext & RenderContext) !== NoContext) {
1058-
console.error(
1059-
'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
1060-
'already rendering.',
1061-
);
1062-
}
1063-
}
1064-
// We're already rendering, so we can't synchronously flush pending work.
1065-
// This is probably a nested event dispatch triggered by a lifecycle/effect,
1066-
// like `el.focus()`. Exit.
1067-
return;
1068-
}
1069-
flushSyncCallbacks();
1070-
// If the discrete updates scheduled passive effects, flush them now so that
1071-
// they fire before the next serial event.
1072-
flushPassiveEffects();
1073-
}
1074-
10751047
export function deferredUpdates<A>(fn: () => A): A {
10761048
const previousPriority = getCurrentUpdatePriority();
10771049
const prevTransition = ReactCurrentBatchConfig.transition;
@@ -1142,7 +1114,10 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
11421114
}
11431115
}
11441116

1145-
export function flushSync<A, R>(fn: A => R, a: A): R {
1117+
export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1118+
fn: A => R,
1119+
a: A,
1120+
): R {
11461121
const prevExecutionContext = executionContext;
11471122
executionContext |= BatchedContext;
11481123

@@ -1165,18 +1140,23 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11651140
// the stack.
11661141
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
11671142
flushSyncCallbacks();
1168-
} else {
1169-
if (__DEV__) {
1170-
console.error(
1171-
'flushSync was called from inside a lifecycle method. React cannot ' +
1172-
'flush when React is already rendering. Consider moving this call to ' +
1173-
'a scheduler task or micro task.',
1174-
);
1175-
}
11761143
}
11771144
}
11781145
}
11791146

1147+
export function flushSync<A, R>(fn: A => R, a: A): R {
1148+
if (__DEV__) {
1149+
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
1150+
console.error(
1151+
'flushSync was called from inside a lifecycle method. React cannot ' +
1152+
'flush when React is already rendering. Consider moving this call to ' +
1153+
'a scheduler task or micro task.',
1154+
);
1155+
}
1156+
}
1157+
return flushSyncWithoutWarningIfAlreadyRendering(fn, a);
1158+
}
1159+
11801160
export function flushControlled(fn: () => mixed): void {
11811161
const prevExecutionContext = executionContext;
11821162
executionContext |= BatchedContext;

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

+17-37
Original file line numberDiff line numberDiff line change
@@ -1044,34 +1044,6 @@ export function getExecutionContext(): ExecutionContext {
10441044
return executionContext;
10451045
}
10461046

1047-
export function flushDiscreteUpdates() {
1048-
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
1049-
// However, `act` uses `batchedUpdates`, so there's no way to distinguish
1050-
// those two cases. Need to fix this before exposing flushDiscreteUpdates
1051-
// as a public API.
1052-
if (
1053-
(executionContext & (BatchedContext | RenderContext | CommitContext)) !==
1054-
NoContext
1055-
) {
1056-
if (__DEV__) {
1057-
if ((executionContext & RenderContext) !== NoContext) {
1058-
console.error(
1059-
'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
1060-
'already rendering.',
1061-
);
1062-
}
1063-
}
1064-
// We're already rendering, so we can't synchronously flush pending work.
1065-
// This is probably a nested event dispatch triggered by a lifecycle/effect,
1066-
// like `el.focus()`. Exit.
1067-
return;
1068-
}
1069-
flushSyncCallbacks();
1070-
// If the discrete updates scheduled passive effects, flush them now so that
1071-
// they fire before the next serial event.
1072-
flushPassiveEffects();
1073-
}
1074-
10751047
export function deferredUpdates<A>(fn: () => A): A {
10761048
const previousPriority = getCurrentUpdatePriority();
10771049
const prevTransition = ReactCurrentBatchConfig.transition;
@@ -1142,7 +1114,10 @@ export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
11421114
}
11431115
}
11441116

1145-
export function flushSync<A, R>(fn: A => R, a: A): R {
1117+
export function flushSyncWithoutWarningIfAlreadyRendering<A, R>(
1118+
fn: A => R,
1119+
a: A,
1120+
): R {
11461121
const prevExecutionContext = executionContext;
11471122
executionContext |= BatchedContext;
11481123

@@ -1165,18 +1140,23 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11651140
// the stack.
11661141
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
11671142
flushSyncCallbacks();
1168-
} else {
1169-
if (__DEV__) {
1170-
console.error(
1171-
'flushSync was called from inside a lifecycle method. React cannot ' +
1172-
'flush when React is already rendering. Consider moving this call to ' +
1173-
'a scheduler task or micro task.',
1174-
);
1175-
}
11761143
}
11771144
}
11781145
}
11791146

1147+
export function flushSync<A, R>(fn: A => R, a: A): R {
1148+
if (__DEV__) {
1149+
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
1150+
console.error(
1151+
'flushSync was called from inside a lifecycle method. React cannot ' +
1152+
'flush when React is already rendering. Consider moving this call to ' +
1153+
'a scheduler task or micro task.',
1154+
);
1155+
}
1156+
}
1157+
return flushSyncWithoutWarningIfAlreadyRendering(fn, a);
1158+
}
1159+
11801160
export function flushControlled(fn: () => mixed): void {
11811161
const prevExecutionContext = executionContext;
11821162
executionContext |= BatchedContext;

packages/react-reconciler/src/__tests__/ReactFlushSync-test.js

+23
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,27 @@ describe('ReactFlushSync', () => {
173173
// Effect flushes after paint.
174174
expect(Scheduler).toHaveYielded(['Effect']);
175175
});
176+
177+
test('does not flush pending passive effects', async () => {
178+
function App() {
179+
useEffect(() => {
180+
Scheduler.unstable_yieldValue('Effect');
181+
}, []);
182+
return <Text text="Child" />;
183+
}
184+
185+
const root = ReactNoop.createRoot();
186+
await act(async () => {
187+
root.render(<App />);
188+
expect(Scheduler).toFlushUntilNextPaint(['Child']);
189+
expect(root).toMatchRenderedOutput('Child');
190+
191+
// Passive effects are pending. Calling flushSync should not affect them.
192+
ReactNoop.flushSync();
193+
// Effects still haven't fired.
194+
expect(Scheduler).toHaveYielded([]);
195+
});
196+
// Now the effects have fired.
197+
expect(Scheduler).toHaveYielded(['Effect']);
198+
});
176199
});

0 commit comments

Comments
 (0)